diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..98825dbd4 --- /dev/null +++ b/test/README.md @@ -0,0 +1,71 @@ +# ProxySQL Test Suite + +This directory contains the ProxySQL test suite, including TAP tests and infrastructure for running them. + +## Quick Start: Running Tests Locally + +To run tests using the local CI infrastructure (Docker-based isolation): + +```bash +# 1. Set up environment +export WORKSPACE=$(pwd) +export INFRA_ID="test-$(date +%s)" # Unique ID using timestamp +export TAP_GROUP="mysql84-g1" # Or another group like "legacy-g1" +export TEST_PY_TAP_INCL="test_name-t" # Optional: filter to specific test +export SKIP_CLUSTER_START=1 # Skip cluster nodes for single-node tests +source test/infra/common/env.sh + +# 2. Start ProxySQL and backends +./test/infra/control/ensure-infras.bash + +# 3. Run the tests +./test/infra/control/run-tests-isolated.bash + +# 4. Cleanup when done +./test/infra/control/stop-proxysql-isolated.bash +``` + +## Documentation + +- **[infra/README.md](infra/README.md)** - Complete documentation for the Unified CI infrastructure +- **[tap/groups/groups.json](tap/groups/groups.json)** - Test group definitions + +## Available Test Groups + +Common test groups (defined in `tap/groups/groups.json`): + +| Group | Description | +|-------|-------------| +| `mysql84-g1` | MySQL 8.4 tests | +| `mysql57-g1` | MySQL 5.7 tests | +| `mariadb10-g1` | MariaDB 10 tests | +| `legacy-g1` | Legacy tests (MySQL 5.7, MariaDB 10, PostgreSQL, ClickHouse) | + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `INFRA_ID` | **Required**. Unique namespace for Docker containers. Use timestamp: `test-$(date +%s)` | +| `TAP_GROUP` | Test group to run (e.g., `mysql84-g1`) | +| `TEST_PY_TAP_INCL` | Regex to filter tests within the group | +| `SKIP_CLUSTER_START` | Set to `1` to skip starting additional ProxySQL nodes | + +## Prerequisites + +1. Build the CI base image (one-time setup): + ```bash + cd test/infra/docker-base + docker build --network host -t proxysql-ci-base:latest . + cd ../../../ + ``` + +2. Build ProxySQL and TAP tests: + ```bash + make -j$(nproc) && make -j$(nproc) build_tap_test + ``` + +## Troubleshooting + +- **"Directory Not Empty"**: Run `./test/infra/control/stop-proxysql-isolated.bash` with the same `INFRA_ID` +- **Container issues**: Check logs in `ci_infra_logs/${INFRA_ID}/` +- **Test failures**: Check logs in `ci_infra_logs/${INFRA_ID}/tests/` diff --git a/test/tap/tests/max_connections_ff-t.cpp b/test/tap/tests/max_connections_ff-t.cpp index 681fa18bd..adcf44aa0 100644 --- a/test/tap/tests/max_connections_ff-t.cpp +++ b/test/tap/tests/max_connections_ff-t.cpp @@ -1,19 +1,51 @@ /** * @file max_connections_ff-t.cpp - * @brief This test verifies that 'max_connections' is honored by 'ff' connections. - * @details The test performs multiple checks for this: - * - When 'max_connections' is reached, queries for 'fast_forward' sessions trying to obtain connections - * should timeout in 'mysql-connect_timeout_server'. - * - * IMPORTANT-NOTE: Since second test is relying on 'stats_mysql_connection_pool' for checking the correct - * creation and destruction of connections, it's important to make sure that connections used between the - * two tests are *NOT COMPATIBLE*. This way we can ensure that stats from 'stats_mysql_connection_pool' - * actually correspond to the second test, and are not 'FreeConns' left from the previous test that can be - * reused, thus messing the stats. For this we impose: 'CLIENT_IGNORE_SPACE' to connections created in this - * test. - * - * - When only one non-suited 'free' connection is left, a 'fast_forward' session shouldn't try to create - * another new connection without first destroying the 'free' connection left. + * @brief Test to verify that 'max_connections' server setting is properly honored by 'fast_forward' connections. + * + * @details This test verifies that ProxySQL correctly enforces the 'max_connections' limit configured + * per MySQL server when handling 'fast_forward' (ff) connections. Fast forward connections are a special + * mode in ProxySQL where the connection is essentially passed through directly to the backend server + * with minimal intervention. + * + * The test consists of two main test scenarios: + * + * ## Test 1: test_ff_sess_exceeds_max_conns() + * This test verifies that when 'max_connections' limit is reached, new 'fast_forward' sessions + * trying to obtain backend connections should timeout with error after 'mysql-connect_timeout_server_max' + * milliseconds. The test: + * 1. Sets a specific 'max_connections' limit on the target server + * 2. Enables 'fast_forward' for the test user + * 3. Creates max_connections number of connections with open transactions (holding them) + * 4. Attempts to create one more fast_forward connection and execute a query + * 5. Verifies the query fails with a timeout within expected time bounds + * + * ## Test 2: test_ff_only_one_free_conn() + * This test verifies that when a 'free' (unused) connection exists in the pool that is NOT compatible + * with a new fast_forward session, ProxySQL correctly destroys the incompatible free connection before + * creating a new one. The test: + * 1. Sets a specific 'max_connections' limit + * 2. Enables 'fast_forward' for the test user + * 3. Creates max_connections connections with open transactions + * 4. Commits one transaction to leave one 'FreeConn' in the pool + * 5. Creates a new fast_forward connection and executes a query + * 6. Verifies via 'stats_mysql_connection_pool' that the stats are correct (old free conn destroyed, new one created) + * + * IMPORTANT-NOTE: Since the second test relies on 'stats_mysql_connection_pool' for checking the correct + * creation and destruction of connections, it's important to make sure that connections used between the + * two tests are *NOT COMPATIBLE*. This way we can ensure that stats from 'stats_mysql_connection_pool' + * actually correspond to the second test, and are not 'FreeConns' left from the previous test that can be + * reused, thus messing the stats. For this we impose: 'CLIENT_IGNORE_SPACE' flag to connections created + * in the first test, making them incompatible with the second test's connections. + * + * @test Test 1a: max_connections=1, timeout=8000ms - verify timeout behavior + * @test Test 1b: max_connections=3, timeout=2000ms - verify timeout behavior with multiple conns + * @test Test 2a: max_connections=1 - verify free connection handling + * @test Test 2b: max_connections=3 - verify free connection handling with multiple conns + * + * @pre Requires 'regular_infra' CI configuration with: + * - A MySQL server configured in some hostgroup + * - User 'sbtest1' with password 'sbtest1' configured in mysql_users + * - The test dynamically discovers the default_hostgroup for user 'sbtest1' */ #include @@ -40,37 +72,54 @@ using hrc = std::chrono::high_resolution_clock; using nlohmann::json; /** - * @brief Get the default hostgroup for a user from mysql_users + * @brief Discovers the default hostgroup for a given user from ProxySQL. + * + * This function queries the mysql_users table to find the default_hostgroup + * configured for the specified user. This allows tests to work with any + * infrastructure configuration without hardcoding hostgroup values. + * + * @param proxy_admin Admin connection to ProxySQL + * @param username The username to query + * @return The discovered default hostgroup, or -1 if not found */ -int get_user_default_hostgroup(MYSQL* admin, const string& username) { - string query = "SELECT default_hostgroup FROM mysql_users WHERE username='" + username + "'"; - diag("Executing query `%s`...", query.c_str()); - - if (mysql_query(admin, query.c_str()) != 0) { - diag("Failed to query user default_hostgroup: %s", mysql_error(admin)); - return -1; - } - - MYSQL_RES* res = mysql_store_result(admin); - if (res == NULL) { - diag("Failed to store result: %s", mysql_error(admin)); - return -1; +int discover_user_hostgroup(MYSQL* proxy_admin, const string& username) { + string hg_query = "SELECT default_hostgroup FROM mysql_users WHERE username='" + username + "' LIMIT 1"; + MYSQL_RES* res = nullptr; + int target_hg = -1; + + if (mysql_query(proxy_admin, hg_query.c_str()) == 0) { + res = mysql_store_result(proxy_admin); + if (res) { + MYSQL_ROW row = mysql_fetch_row(res); + if (row) target_hg = atoi(row[0]); + mysql_free_result(res); + } } - MYSQL_ROW row = mysql_fetch_row(res); - int hg = -1; - if (row && row[0]) { - hg = atoi(row[0]); + if (target_hg < 0) { + diag("WARNING: Could not discover default_hostgroup for user '%s'", username.c_str()); } - mysql_free_result(res); - - return hg; + return target_hg; } +/** + * @brief Creates multiple MySQL connections with open transactions to hold backend connections. + * + * This function creates 'n' MySQL connections to ProxySQL and starts a transaction on each one. + * The open transactions prevent the connections from being returned to the pool, effectively + * holding 'max_connections' backend connections and preventing new ones from being created. + * + * @param cl Command line containing connection parameters (host, port, username, password) + * @param n Number of connections/transactions to create + * @param out_conns Output vector to store the created MYSQL connection handles + * @param client_flags MySQL client flags to use when connecting (default: 0) + * @return EXIT_SUCCESS on success, EXIT_FAILURE on any connection failure + * + * @note The caller is responsible for closing these connections when done + */ int create_n_trxs(const CommandLine& cl, size_t n, vector& out_conns, int client_flags = 0) { diag("Creating '%ld' transactions to test 'max_connections'", n); - diag(" - Connecting to: %s:%d", cl.host, cl.port); - diag(" - Username: %s", cl.username); + diag("Each transaction holds a backend connection, preventing it from being reused"); vector res_conns {}; @@ -80,22 +129,32 @@ int create_n_trxs(const CommandLine& cl, size_t n, vector& out_conns, in fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_mysql)); return EXIT_FAILURE; } - - diag(" - Created connection %zu/%zu", i + 1, n); + mysql_query(proxy_mysql, "BEGIN"); res_conns.push_back(proxy_mysql); } - diag("Successfully created %zu transaction connections", n); out_conns = res_conns; return EXIT_SUCCESS; } +/** + * @brief Sets the 'max_connections' limit for all servers in a specific hostgroup. + * + * This function updates the mysql_servers table to set a new max_connections value + * for the specified hostgroup, then loads the configuration to runtime. + * + * @param proxy_admin Admin connection to ProxySQL + * @param max_conns The maximum number of connections allowed per server + * @param hg_id The hostgroup ID to update + * @return EXIT_SUCCESS on success + */ int set_max_conns(MYSQL* proxy_admin, int max_conns, int hg_id) { string max_conn_query {}; string_format("UPDATE mysql_servers SET max_connections=%d WHERE hostgroup_id=%d", max_conn_query, max_conns, hg_id); + diag("Setting max_connections=%d for hostgroup %d (limit backend conns per server)", max_conns, hg_id); diag("Executing query `%s`...", max_conn_query.c_str()); MYSQL_QUERY(proxy_admin, max_conn_query.c_str()); @@ -105,10 +164,22 @@ int set_max_conns(MYSQL* proxy_admin, int max_conns, int hg_id) { return EXIT_SUCCESS; } +/** + * @brief Sets the 'mysql-connect_timeout_server_max' global variable. + * + * This variable controls the maximum time (in milliseconds) ProxySQL will wait + * when trying to establish a connection to a backend server. When max_connections + * limit is reached, new connection attempts will timeout after this duration. + * + * @param proxy_admin Admin connection to ProxySQL + * @param connect_to Timeout value in milliseconds + * @return EXIT_SUCCESS on success + */ int set_srv_conn_to(MYSQL* proxy_admin, int connect_to) { string srv_conn_to_query {}; string_format("SET mysql-connect_timeout_server_max=%d", srv_conn_to_query, connect_to); + diag("Setting mysql-connect_timeout_server_max=%d ms (timeout for new backend connections)", connect_to); diag("Executing query `%s`...", srv_conn_to_query.c_str()); MYSQL_QUERY(proxy_admin, srv_conn_to_query.c_str()); @@ -118,11 +189,24 @@ int set_srv_conn_to(MYSQL* proxy_admin, int connect_to) { return EXIT_SUCCESS; } - +/** + * @brief Enables or disables 'fast_forward' mode for a specific user. + * + * Fast forward mode is a special ProxySQL feature where the proxy essentially + * passes through the client connection directly to the backend server with + * minimal intervention. This is useful for specific use cases like MySQL + * replication connections or when ProxySQL's query processing is not needed. + * + * @param proxy_admin Admin connection to ProxySQL + * @param user The username to update + * @param ff true to enable fast_forward, false to disable + * @return EXIT_SUCCESS on success + */ int set_ff_for_user(MYSQL* proxy_admin, const string& user, bool ff) { string upd_ff_query {}; string_format("UPDATE mysql_users SET fast_forward=%d WHERE username='%s'", upd_ff_query, ff, user.c_str()); + diag("Setting fast_forward=%d for user '%s' (enable/disable pass-through mode)", ff, user.c_str()); diag("Executing query `%s`...", upd_ff_query.c_str()); MYSQL_QUERY(proxy_admin, upd_ff_query.c_str()); @@ -132,10 +216,33 @@ int set_ff_for_user(MYSQL* proxy_admin, const string& user, bool ff) { return EXIT_SUCCESS; } +/** + * SQL query template to retrieve connection pool statistics for a specific hostgroup. + * Returns: ConnUsed, ConnFree, ConnOk, ConnERR, MaxConnUsed + * + * - ConnUsed: Number of connections currently in use (held by client sessions) + * - ConnFree: Number of idle connections in the pool available for reuse + * - ConnOk: Total number of successful connections created + * - ConnERR: Total number of failed connection attempts + * - MaxConnUsed: Maximum number of connections used concurrently + */ const char* CONNPOOL_STATS { "SELECT ConnUsed,ConnFree,ConnOk,ConnERR,MaxConnUsed FROM stats.stats_mysql_connection_pool WHERE hostgroup=%d" }; +/** + * @brief Retrieves connection pool statistics for a specific hostgroup. + * + * This function queries the stats.stats_mysql_connection_pool table to get + * current statistics about the connection pool for the specified hostgroup. + * + * @param proxy_admin Admin connection to ProxySQL + * @param hg_id The hostgroup ID to query stats for + * @param out_stats Output vector containing the 5 stat columns: ConnUsed, ConnFree, ConnOk, ConnERR, MaxConnUsed + * @return EXIT_SUCCESS on success, EXIT_FAILURE if query fails or no rows returned + * + * @note Expects exactly one row (assumes 'regular_infra' with single server per hostgroup) + */ int conn_pool_hg_stats(MYSQL* proxy_admin, int hg_id, vector& out_stats) { MYSQL_RES* my_stats_res = NULL; @@ -170,20 +277,42 @@ cleanup: return err; } -int test_ff_sess_exceeds_max_conns(const CommandLine& cl, MYSQL* proxy_admin, int tg_hg, const string& username, long srv_conn_to, int max_conns) { - diag("================================================================================"); - diag("TEST: test_ff_sess_exceeds_max_conns"); - diag("================================================================================"); - diag("This test verifies that 'max_connections' is honored by 'fast_forward' sessions."); - diag("When 'max_connections' is reached, new 'fast_forward' sessions should timeout."); - diag(""); - diag("Test parameters:"); - diag(" - Target hostgroup: %d", tg_hg); - diag(" - Test username: %s", username.c_str()); - diag(" - Server connect timeout (ms): %ld", srv_conn_to); - diag(" - Max connections: %d", max_conns); - diag("================================================================================"); - diag(""); +/** + * @brief Tests that fast_forward sessions timeout when max_connections limit is reached. + * + * This test verifies the following behavior: + * 1. When max_connections limit is reached (all connections held by open transactions) + * 2. A new fast_forward session attempting to execute a query + * 3. Should timeout after 'mysql-connect_timeout_server_max' milliseconds + * 4. Should NOT exceed the timeout by more than poll_timeout + grace period + * + * Test procedure: + * - Set max_connections to the specified value for hostgroup 0 + * - Enable fast_forward for user 'sbtest1' + * - Create max_connections connections with open transactions (holding all available slots) + * - Create one more fast_forward connection and attempt a query + * - Measure the time until the query fails + * - Verify the timeout is within expected bounds + * + * @param cl Command line with connection parameters + * @param proxy_admin Admin connection to ProxySQL + * @param srv_conn_to The timeout value in milliseconds to set for mysql-connect_timeout_server_max + * @param max_conns The max_connections limit to set for the server + * @return EXIT_SUCCESS on test completion (check pass/fail via ok() results) + */ +int test_ff_sess_exceeds_max_conns(const CommandLine& cl, MYSQL* proxy_admin, long srv_conn_to, int max_conns) { + diag("=== TEST: test_ff_sess_exceeds_max_conns ==="); + diag("Testing fast_forward timeout when max_connections (%d) is exceeded", max_conns); + diag("Expected: new ff connection should timeout after ~%ld ms", srv_conn_to); + + // Dynamically discover the target hostgroup for user 'sbtest1' + const string username = "sbtest1"; + int tg_hg = discover_user_hostgroup(proxy_admin, username); + if (tg_hg < 0) { + diag("Failed to discover hostgroup for user '%s'", username.c_str()); + return EXIT_FAILURE; + } + diag("Discovered target hostgroup for user '%s': %d", username.c_str(), tg_hg); string str_poll_timeout {}; string str_connect_timeout_server {}; @@ -249,6 +378,7 @@ int test_ff_sess_exceeds_max_conns(const CommandLine& cl, MYSQL* proxy_admin, in } // See 'IMPORTANT-NOTE' on file @details. + diag("Creating %d connections with CLIENT_IGNORE_SPACE flag (incompatible with test 2)", max_conns); my_err = create_n_trxs(cl, max_conns, trx_conns, CLIENT_IGNORE_SPACE); if (my_err) { diag("Failed to create the required '%d' transactions", max_conns); @@ -257,6 +387,8 @@ int test_ff_sess_exceeds_max_conns(const CommandLine& cl, MYSQL* proxy_admin, in } // Create a new ff connection and check that a query expires after 'connection' + diag("All %d backend connections are now held. Creating one more fast_forward connection...", max_conns); + diag("This new connection should timeout since max_connections limit is reached"); { MYSQL* proxy_ff = mysql_init(NULL); if (!mysql_real_connect(proxy_ff, cl.host, username.c_str(), username.c_str(), NULL, cl.port, NULL, 0)) { @@ -268,6 +400,7 @@ int test_ff_sess_exceeds_max_conns(const CommandLine& cl, MYSQL* proxy_admin, in std::chrono::nanoseconds duration; hrc::time_point start = hrc::now(); + diag("Executing query on the (n+1)th fast_forward connection..."); int q_err = mysql_query(proxy_ff, "DO 1"); int m_errno = mysql_errno(proxy_ff); const char* m_error = mysql_error(proxy_ff); @@ -277,6 +410,9 @@ int test_ff_sess_exceeds_max_conns(const CommandLine& cl, MYSQL* proxy_admin, in duration = end - start; double duration_s = duration.count() / pow(10,9); + diag("Query completed - Error: %d, ErrMsg: %s", m_errno, m_error); + diag("Time waited: %lf seconds", duration_s); + const double srv_conn_to_s = connect_timeout / 1000.0; const double poll_to_s = poll_timeout / 1000.0; const double grace = 500 / 1000.0; @@ -312,25 +448,56 @@ cleanup: return EXIT_SUCCESS; } -int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg_hg, const string& username, int max_conns) { - diag("================================================================================"); - diag("TEST: test_ff_only_one_free_conn"); - diag("================================================================================"); - diag("This test verifies that 'fast_forward' sessions properly reuse 'FreeConn'"); - diag("connections from the connection pool instead of creating new connections."); - diag(""); - diag("Test parameters:"); - diag(" - Target hostgroup: %d", tg_hg); - diag(" - Test username: %s", username.c_str()); - diag(" - Max connections: %d", max_conns); - diag("================================================================================"); - diag(""); +/** + * @brief Tests that fast_forward sessions properly handle free (incompatible) connections. + * + * This test verifies that when a 'free' connection exists in the pool but is not compatible + * with a new fast_forward session request, ProxySQL correctly: + * 1. Destroys the incompatible free connection + * 2. Creates a new connection for the fast_forward session + * 3. Updates connection pool statistics correctly + * + * The key scenario being tested: + * - max_connections limit is reached + * - One connection is released (becomes 'FreeConn') + * - A new fast_forward session is created + * - The new session should NOT reuse the incompatible free connection + * - Instead, the free connection should be destroyed and a new one created + * + * Test procedure: + * - Set max_connections to the specified value for hostgroup 0 + * - Enable fast_forward for user 'sbtest1' + * - Reset connection pool stats + * - Create max_connections connections with open transactions + * - Commit one transaction to release one connection (now 'FreeConn') + * - Verify stats show ConnUsed=max_conns-1, ConnFree=1 + * - Create a new fast_forward connection and execute a query + * - Verify stats show the connection was properly handled + * + * @param cl Command line with connection parameters + * @param proxy_admin Admin connection to ProxySQL + * @param max_conns The max_connections limit to set for the server + * @return EXIT_SUCCESS on test completion (check pass/fail via ok() results) + */ +int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int max_conns) { + diag("=== TEST: test_ff_only_one_free_conn ==="); + diag("Testing fast_forward behavior when one free (but incompatible) connection exists"); + diag("Max connections: %d - one will be freed, then reused/destroyed", max_conns); if (proxy_admin == NULL || max_conns == 0) { diag("'test_ff_only_one_free_conn' received invalid params."); return EINVAL; } + // Dynamically discover the target hostgroup for user 'sbtest1' + const string username = "sbtest1"; + int tg_hg = discover_user_hostgroup(proxy_admin, username); + if (tg_hg < 0) { + diag("Failed to discover hostgroup for user '%s'", username.c_str()); + return EXIT_FAILURE; + } + diag("Discovered target hostgroup for user '%s': %d", username.c_str(), tg_hg); + const char* reset_connpool_stats { "SELECT * FROM stats.stats_mysql_connection_pool_reset" }; string str_poll_timeout {}; @@ -363,8 +530,9 @@ int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg } // Reset all the current stats for 'stats_mysql_connection_pool' + diag("Resetting connection pool stats to get clean measurements"); my_err = mysql_query(proxy_admin, reset_connpool_stats); - diag("Executing query `%s` in new 'fast_forward' conn...", reset_connpool_stats); + diag("Executing query `%s`...", reset_connpool_stats); if (my_err) { diag("Query '%s' failed", reset_connpool_stats); res = EXIT_FAILURE; @@ -372,6 +540,7 @@ int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg } mysql_free_result(mysql_store_result(proxy_admin)); + diag("Creating %d connections with open transactions to fill the pool", max_conns); my_err = create_n_trxs(cl, max_conns, trx_conns); if (my_err) { diag("Failed to create the required '%d' transactions", max_conns); @@ -381,6 +550,7 @@ int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg { // 1. First leave one connection 'Free' and verify it via 'stats_mysql_connection_pool' + diag("Step 1: Releasing one connection to create a 'FreeConn' in the pool"); MYSQL* trx_conn = trx_conns.back(); diag("Freeing ONE connection by committing the transaction..."); @@ -396,6 +566,7 @@ int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg } // 2. Verify there are 'max_connections - 1' as 'ConnUsed' and just one 'ConnFree' + diag("Step 2: Verifying pool stats - expect ConnUsed=%d, ConnFree=1", max_conns - 1); vector hg_stats_row {}; my_err = conn_pool_hg_stats(proxy_admin, tg_hg, hg_stats_row); if (my_err) { @@ -416,6 +587,9 @@ int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg ); // 3. Create a new connection with a different user using 'fast_forward' + diag("Step 3: Creating a NEW fast_forward connection"); + diag("This connection should NOT reuse the existing FreeConn (incompatible)"); + diag("Instead, the FreeConn should be destroyed and a new connection created"); diag("Creating new 'fast_forward' connection using user '%s'", username.c_str()); MYSQL* proxy_ff = mysql_init(NULL); @@ -426,6 +600,7 @@ int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg } // 3.1 Issue a simple query into the new 'fast_forward' connection + diag("Step 3.1: Executing query on the new fast_forward connection"); diag("Executing query `%s` in new 'fast_forward' conn...", "DO 1"); int q_my_err = mysql_query(proxy_ff, "DO 1"); if (q_my_err) { @@ -437,7 +612,9 @@ int test_ff_only_one_free_conn(const CommandLine& cl, MYSQL* proxy_admin, int tg } // 3.2 Check the stats have properly changed due to this new connection - diag("Checking 'stats_mysql_connection_pool' changed properly after query to 'fast_forward' session"); + diag("Step 3.2: Verifying final pool stats after fast_forward query"); + diag("Expected: ConnUsed=%d (all held), ConnFree=0, ConnOk=%d (total created), ConnErr=0", + max_conns, max_conns + 1); my_err = conn_pool_hg_stats(proxy_admin, tg_hg, hg_stats_row); if (my_err) { res = EXIT_FAILURE; @@ -483,9 +660,37 @@ cleanup: return EXIT_SUCCESS; } +/** + * @brief Main entry point for the max_connections_ff test. + * + * This test verifies that ProxySQL correctly enforces the 'max_connections' limit + * for servers when handling 'fast_forward' connections. + * + * Test execution: + * 1. Connect to ProxySQL admin interface + * 2. Run test_ff_sess_exceeds_max_conns with max_conns=1, timeout=8000ms + * 3. Run test_ff_sess_exceeds_max_conns with max_conns=3, timeout=2000ms + * 4. Run test_ff_only_one_free_conn with max_conns=1 + * 5. Run test_ff_only_one_free_conn with max_conns=3 + * + * Total tests: 6 (1 check per test_ff_sess_exceeds_max_conns * 2 runs + 2 checks per test_ff_only_one_free_conn * 2 runs) + * + * @param argc Argument count + * @param argv Argument values + * @return Exit status from TAP framework + */ int main(int argc, char** argv) { CommandLine cl; + diag("==========================================================="); + diag("TEST: max_connections_ff - Fast Forward Connection Limits"); + diag("==========================================================="); + diag("This test verifies 'max_connections' is honored by ff connections"); + diag(""); + + // 'test_ff_sess_exceeds_max_conns' performs '1' check, 'test_ff_only_one_free_conn' performs '2' checks + plan(1 * 2 + 2 * 2); + if (cl.getEnv()) { diag("Failed to get the required environmental variables."); return EXIT_FAILURE; @@ -496,66 +701,46 @@ int main(int argc, char** argv) { 2*2 // 'test_ff_only_one_free_conn' ); - // Verbose test header - diag("================================================================================"); - diag("Test: max_connections_ff-t"); - diag("================================================================================"); - diag("This test verifies that 'max_connections' is properly honored by 'fast_forward'"); - diag("connections in ProxySQL."); - diag(""); - diag("Test scenarios:"); - diag(" 1. Test that exceeding max_connections causes timeout for new ff sessions"); - diag(" 2. Test that ff sessions properly reuse free connections from pool"); - diag(""); - diag("Configuration:"); - diag(" - ProxySQL admin host: %s", cl.admin_host); - diag(" - ProxySQL admin port: %d", cl.admin_port); - diag(" - ProxySQL data host: %s", cl.host); - diag(" - ProxySQL data port: %d", cl.port); - diag(" - Test username: %s", cl.username); - diag("================================================================================"); - diag(""); - MYSQL* proxy_admin = mysql_init(NULL); - if (!mysql_real_connect(proxy_admin, cl.admin_host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + if (!mysql_real_connect(proxy_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(proxy_admin)); return EXIT_FAILURE; } - // Get the default hostgroup for the test user - int tg_hg = get_user_default_hostgroup(proxy_admin, cl.username); - if (tg_hg < 0) { - diag("ERROR: Failed to get default_hostgroup for user '%s'", cl.username); - return EXIT_FAILURE; - } - diag("User '%s' default_hostgroup: %d", cl.username, tg_hg); - - // Check if the target hostgroup has servers configured - { - diag("Checking if hostgroup %d has servers configured...", tg_hg); - string query = "SELECT hostgroup_id, hostname, port, status FROM mysql_servers WHERE hostgroup_id=" + std::to_string(tg_hg); - MYSQL_QUERY(proxy_admin, query.c_str()); - MYSQL_RES* res = mysql_store_result(proxy_admin); - int rows = mysql_num_rows(res); - if (rows == 0) { - diag("ERROR: No servers found in hostgroup %d", tg_hg); - mysql_free_result(res); - return EXIT_FAILURE; - } - diag("Found %d server(s) in hostgroup %d", rows, tg_hg); - mysql_free_result(res); - } - + diag("Connected to ProxySQL admin interface at %s:%d", cl.host, cl.admin_port); diag(""); // 1. Test for: '8000' timeout, '1' max_connections - test_ff_sess_exceeds_max_conns(cl, proxy_admin, tg_hg, cl.username, 8000, 1); + diag("-----------------------------------------------------------"); + diag("TEST RUN 1: max_conns=1, connect_timeout=8000ms"); + diag("-----------------------------------------------------------"); + test_ff_sess_exceeds_max_conns(cl, proxy_admin, 8000, 1); + // 2. Test for: '2000' timeout, '3' max_connections - test_ff_sess_exceeds_max_conns(cl, proxy_admin, tg_hg, cl.username, 2000, 3); + diag(""); + diag("-----------------------------------------------------------"); + diag("TEST RUN 2: max_conns=3, connect_timeout=2000ms"); + diag("-----------------------------------------------------------"); + test_ff_sess_exceeds_max_conns(cl, proxy_admin, 2000, 3); + // 3. Test for only one 'FreeConn' that should be destroyed due to incoming 'fast_forward' conn - MaxConn: 1 - test_ff_only_one_free_conn(cl, proxy_admin, tg_hg, cl.username, 1); + diag(""); + diag("-----------------------------------------------------------"); + diag("TEST RUN 3: test_ff_only_one_free_conn with max_conns=1"); + diag("-----------------------------------------------------------"); + test_ff_only_one_free_conn(cl, proxy_admin, 1); + // 4. Test for only one 'FreeConn' that should be destroyed due to incoming 'fast_forward' conn - MaxConn: 3 - test_ff_only_one_free_conn(cl, proxy_admin, tg_hg, cl.username, 3); + diag(""); + diag("-----------------------------------------------------------"); + diag("TEST RUN 4: test_ff_only_one_free_conn with max_conns=3"); + diag("-----------------------------------------------------------"); + test_ff_only_one_free_conn(cl, proxy_admin, 3); + + diag(""); + diag("==========================================================="); + diag("All tests completed"); + diag("==========================================================="); mysql_close(proxy_admin);