diff --git a/CLAUDE.md b/CLAUDE.md index e076a2a4b..7b0ecf78a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,22 +55,39 @@ The same codebase produces three product tiers via feature flags: Tests use TAP (Test Anything Protocol) with Docker-based backend infrastructure. +### Running TAP tests — DO NOT manually set up Docker containers + +**ALWAYS use `run-tests-isolated.bash`**. It handles infrastructure setup, ProxySQL start, test execution, and cleanup. Never manually create Docker networks, start containers, or run init scripts — the runner does all of that. + ```bash -# Build and run all TAP tests -make build_tap_tests -cd test/tap && make +# Set up infrastructure (backends + ProxySQL container) +WORKSPACE=$(pwd) INFRA_ID=dev-$USER TAP_GROUP=mysql84-g1 test/infra/control/ensure-infras.bash -# Run specific test groups -cd test/tap/tests && make -cd test/tap/tests_with_deps && make +# Run all tests for a TAP group +WORKSPACE=$(pwd) INFRA_ID=dev-$USER TAP_GROUP=mysql84-g1 test/infra/control/run-tests-isolated.bash -# Test infrastructure (Docker environments) -# Located in test/infra/ with docker-compose configs for: -# mysql57, mysql84, mariadb10, pgsql16, pgsql17, clickhouse23, etc. +# Build test binaries first (requires proxysql binary) +make build_tap_tests # release +make build_tap_test_debug # debug ``` +Available TAP groups are defined in `test/tap/groups/groups.json`. Group names follow the pattern `-g` (e.g., `mysql84-g1`, `legacy-g2`, `pgsql16-g1`). + +### DO NOT + +- **DO NOT** manually create Docker networks (`docker network create`) +- **DO NOT** manually start containers (`docker start`, `docker run`) +- **DO NOT** run `docker-compose-init.bash` directly — use `ensure-infras.bash` +- **DO NOT** symlink build artifacts between worktrees — build in each worktree separately +- **DO NOT** copy source files between worktrees or repos +- **DO NOT** run `cd test/tap/tests && make` and expect tests to pass without infrastructure + +### Test file conventions + Test files follow the naming pattern `test_*.cpp` or `*-t.cpp` in `test/tap/tests/`. +Test binaries are built via a pattern rule in `test/tap/tests/Makefile`: `make -t` compiles `-t.cpp` into `-t`. No special Makefile target is needed for new tests — just add the `.cpp` file and register it in `groups.json`. + ## Architecture ### Build Pipeline diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index 5d9a7adb9..6baaf586c 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -2300,7 +2300,7 @@ void MySQL_Protocol::PPHR_SetConnAttrs(MyProt_tmp_auth_vars& vars1, account_deta const uint8_t zstd_compression_level = (vars1.zstd_compression_level > 0 && vars1.zstd_compression_level <= ZSTD_maxCLevel()) ? vars1.zstd_compression_level - : static_cast(std::min(ZSTD_maxCLevel(), std::max(1, mysql_thread___zstd_compression_level))); + : static_cast(mysql_thread___zstd_compression_level); myconn->options.compression_zstd = false; myconn->options.zstd_compression_level = 0; diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 69ca83c33..684a8db50 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -100,6 +100,7 @@ "mysql-test_ssl_CA-t" : [ "legacy-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","mysql90-g1","mysql95-g1" ], "mysql-watchdog_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4","mysql84-g4","mysql90-g4","mysql95-g4" ], "mysql-zstd_compression_level-t" : [ "legacy-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","mysql90-g1","mysql95-g1" ], + "mysql-zstd_compression_level_libmysql-t" : [ "mysql84-g1","mysql90-g1","mysql95-g1" ], "mysql_encode_unit-t" : [ "unit-tests-g1" ], "mysql_error_classifier_unit-t" : [ "unit-tests-g1" ], "mysql_hostgroup_attributes-servers_defaults-t" : [ "legacy-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","mysql90-g1","mysql95-g1" ], diff --git a/test/tap/tests/Makefile b/test/tap/tests/Makefile index 06baf1711..c07f23780 100644 --- a/test/tap/tests/Makefile +++ b/test/tap/tests/Makefile @@ -263,6 +263,9 @@ mysql_reconnect_libmariadb-t: mysql_reconnect.cpp $(TAP_LDIR)/libtap.so mysql_reconnect_libmysql-t: mysql_reconnect.cpp $(TAP_LDIR)/libtap_mysql8.a $(CXX) -DLIBMYSQL_HELPER8 -DDISABLE_WARNING_COUNT_LOGGING $< -I$(TEST_MYSQL8_IDIR) -I$(TEST_MYSQL8_EDIR) -L$(TEST_MYSQL8_LDIR) -lmysqlclient -ltap_mysql8 -lresolv $(CUSTOMARGS) -o $@ +mysql-zstd_compression_level_libmysql-t: mysql-zstd_compression_level-t.cpp $(TAP_LDIR)/libtap_mysql8.a + $(CXX) -DLIBMYSQL_HELPER8 -DDISABLE_WARNING_COUNT_LOGGING $< -I$(TEST_MYSQL8_IDIR) -I$(TEST_MYSQL8_EDIR) -L$(TEST_MYSQL8_LDIR) -lmysqlclient -ltap_mysql8 -lresolv $(CUSTOMARGS) -o $@ + fast_forward_grace_close_libmysql-t: fast_forward_grace_close.cpp $(TAP_LDIR)/libtap_mysql8.a $(CXX) -DLIBMYSQL_HELPER8 -DDISABLE_WARNING_COUNT_LOGGING $< -I$(TEST_MYSQL8_IDIR) -I$(TEST_MYSQL8_EDIR) -L$(TEST_MYSQL8_LDIR) -lmysqlclient -ltap_mysql8 -lresolv $(CUSTOMARGS) -o $@ diff --git a/test/tap/tests/mysql-zstd_compression_level-t.cpp b/test/tap/tests/mysql-zstd_compression_level-t.cpp index 2b1814ca3..9a0071bbe 100644 --- a/test/tap/tests/mysql-zstd_compression_level-t.cpp +++ b/test/tap/tests/mysql-zstd_compression_level-t.cpp @@ -6,13 +6,24 @@ * - Can be set and loaded to runtime * - Rejects out-of-range values (0, 23) * - Is independent from mysql-protocol_compression_level - * - Compressed connections still work with zstd variable set + * - ZSTD compression is actually used (verified by timing test) + * + * When compiled with LIBMYSQL_HELPER8 (MySQL 8.4 connector): + * - Tests 10-11 use MYSQL_OPT_COMPRESSION_ALGORITHMS for real ZSTD. + * - A cached query is run 1000 times without compression and 1000 times + * with ZSTD. The compressed run must be faster, proving compression + * is active on the client<->ProxySQL link. + * + * When compiled with the default MariaDB connector (no ZSTD client support): + * - Tests 10-11 use mysql CLI via execvp with --compression-algorithms=zstd. */ #include #include #include #include +#include +#include #include "mysql.h" @@ -31,6 +42,43 @@ static int get_variable_value_int(MYSQL* admin, const string& var_name, bool run return atoi(val.c_str()); } +static double elapsed_ms(const struct timeval& start, const struct timeval& end) { + return (end.tv_sec - start.tv_sec) * 1000.0 + (end.tv_usec - start.tv_usec) / 1000.0; +} + +#ifdef LIBMYSQL_HELPER8 + +static MYSQL* create_zstd_connection(const CommandLine& cl, int zstd_level) { + MYSQL* mysql = mysql_init(NULL); + if (!mysql) { + diag("mysql_init() failed"); + return NULL; + } + + const char* algo = "zstd"; + if (mysql_options(mysql, MYSQL_OPT_COMPRESSION_ALGORITHMS, algo)) { + diag("mysql_options(MYSQL_OPT_COMPRESSION_ALGORITHMS, \"zstd\") failed"); + mysql_close(mysql); + return NULL; + } + + if (mysql_options(mysql, MYSQL_OPT_ZSTD_COMPRESSION_LEVEL, &zstd_level)) { + diag("mysql_options(MYSQL_OPT_ZSTD_COMPRESSION_LEVEL, %d) failed", zstd_level); + mysql_close(mysql); + return NULL; + } + + if (!mysql_real_connect(mysql, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + diag("mysql_real_connect with ZSTD compression (level %d) failed: %s", zstd_level, mysql_error(mysql)); + mysql_close(mysql); + return NULL; + } + + return mysql; +} + +#endif + int main(int argc, char** argv) { CommandLine cl; @@ -39,11 +87,25 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } - plan(10); +#ifdef LIBMYSQL_HELPER8 + diag("=== mysql-zstd_compression_level Tests (MySQL 8.4 C API) ==="); + const int num_tests = 11; +#else + const std::string mysql_client = "mysql"; + std::string help_output {}; + const int help_res = execvp(mysql_client, { "mysql", "--help" }, help_output); + const bool mysql_supports_zstd = + help_res == 0 + && help_output.find("compression-algorithms") != std::string::npos + && help_output.find("zstd-compression-level") != std::string::npos; - diag("=== mysql-zstd_compression_level Variable Tests ==="); + diag("=== mysql-zstd_compression_level Tests (MariaDB C API + mysql CLI) ==="); + diag("mysql client supports zstd: %s", mysql_supports_zstd ? "yes" : "no"); + const int num_tests = mysql_supports_zstd ? 11 : 9; +#endif + + plan(num_tests); - // Connect to admin MYSQL* admin = init_mysql_conn(cl.host, cl.admin_port, cl.admin_username, cl.admin_password); if (!admin) { fprintf(stderr, "File %s, line %d, Error: Failed to connect to admin\n", __FILE__, __LINE__); @@ -78,19 +140,22 @@ int main(int argc, char** argv) { ok(rt_val == 22, "Runtime value after SET=22 is 22, got: %d", rt_val); } - // Test 5: Reject value 0 + // Test 5: Reject value 0 (below min=1) { - MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=0"); - MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); - // The value should NOT have changed from 22 (previous test) + int rc = mysql_query(admin, "SET mysql-zstd_compression_level=0"); + if (rc == 0) { + mysql_query(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + } int rt_val = get_variable_value_int(admin, "mysql-zstd_compression_level", true); ok(rt_val == 22, "Value 0 rejected, still 22, got: %d", rt_val); } - // Test 6: Reject value 23 (above max) + // Test 6: Reject value 23 (above max=ZSTD_maxCLevel()) { - MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=23"); - MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + int rc = mysql_query(admin, "SET mysql-zstd_compression_level=23"); + if (rc == 0) { + mysql_query(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + } int rt_val = get_variable_value_int(admin, "mysql-zstd_compression_level", true); ok(rt_val == 22, "Value 23 rejected, still 22, got: %d", rt_val); } @@ -127,24 +192,216 @@ int main(int argc, char** argv) { ok(zlib_rt == 9, "zlib unchanged after zstd change: %d (expect 9)", zlib_rt); } - // Test 10: Functional test - compressed connection still works with zstd variable set + // ======================================================================== + // Tests 10-11: Verify ZSTD compression is actually used + // + // A query rule caches all SELECTs (cache_ttl=60000), eliminating backend + // latency. We run a large resultset query N times on a plain connection + // and N times on a ZSTD-compressed connection. If ZSTD is truly active, + // the compressed transfer will be measurably faster. + // ======================================================================== + +#ifdef LIBMYSQL_HELPER8 + + // Test 10: MySQL 8.4 C API — ZSTD connection succeeds { - MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=3"); + diag("Test 10: C API ZSTD connection using MySQL 8.4 connector"); + diag(" mysql_options(MYSQL_OPT_COMPRESSION_ALGORITHMS, \"zstd\")"); + diag(" mysql_options(MYSQL_OPT_ZSTD_COMPRESSION_LEVEL, 1)"); + MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=1"); MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); - // Connect with CLIENT_COMPRESS (zlib compression). This verifies that the - // new zstd variable does not break existing zlib-compressed connections. - MYSQL* proxy_cmp = init_mysql_conn(cl.host, cl.port, cl.username, cl.password, false, true); - if (proxy_cmp) { - int rc = mysql_query(proxy_cmp, "SELECT 1"); - ok(rc == 0, "Compressed (zlib) connection works with zstd_compression_level=3"); - mysql_close(proxy_cmp); + MYSQL* zstd_conn = create_zstd_connection(cl, 1); + if (zstd_conn) { + unsigned long conn_id = mysql_thread_id(zstd_conn); + diag(" New ZSTD connection: mysql_thread_id() = %lu (no query needed)", conn_id); + ok(true, "C API ZSTD connection established: conn_id=%lu", conn_id); + mysql_close(zstd_conn); } else { - diag("Skipping compressed connection test - connection failed"); - ok(true, "Compressed connection test skipped (connection failed)"); + ok(false, "C API ZSTD connection failed"); + } + } + + // Test 11: ZSTD compression is actually used (timing benchmark) + // + // Uses level 22 (max) to make compression overhead unmistakable. + // On localhost with cached results, a plain connection is fast but + // ZSTD level 22 is CPU-intensive — the timing difference proves + // compression is really happening. + { + diag("Test 11: Verify ZSTD compression by comparing transfer times"); + MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=22"); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + // Cache all SELECTs to eliminate backend latency. + // Use rule_id=1 to ensure this is evaluated before any other rule. + MYSQL_QUERY_T(admin, "DELETE FROM mysql_query_rules WHERE rule_id=1"); + MYSQL_QUERY_T(admin, "INSERT INTO mysql_query_rules (rule_id,active,match_pattern,cache_ttl,apply) " + "VALUES (1,1,'^SELECT',60000,1)"); + MYSQL_QUERY_T(admin, "LOAD MYSQL QUERY RULES TO RUNTIME"); + + const char* bench_query = + "SELECT t.THREAD_ID, t.PROCESSLIST_ID, t.PROCESSLIST_USER, t.PROCESSLIST_HOST, " + "t.NAME, sca.ATTR_NAME, sca.ATTR_VALUE " + "FROM performance_schema.threads t " + "JOIN performance_schema.session_connect_attrs sca " + "ON t.PROCESSLIST_ID = sca.PROCESSLIST_ID"; + const int iterations = 1000; + + // Warm up the cache with one query + MYSQL* warmup = create_zstd_connection(cl, 1); + if (warmup) { + mysql_query(warmup, bench_query); + MYSQL_RES* wr = mysql_store_result(warmup); + my_ulonglong cached_rows = wr ? mysql_num_rows(wr) : 0; + diag(" Cached query returns %lu rows", cached_rows); + if (wr) mysql_free_result(wr); + mysql_close(warmup); + } + + // Benchmark: plain connection (no compression) + MYSQL* plain_conn = mysql_init(NULL); + mysql_real_connect(plain_conn, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0); + struct timeval t0, t1; + gettimeofday(&t0, NULL); + for (int i = 0; i < iterations; i++) { + mysql_query(plain_conn, bench_query); + MYSQL_RES* r = mysql_store_result(plain_conn); + mysql_free_result(r); + } + gettimeofday(&t1, NULL); + double plain_ms = elapsed_ms(t0, t1); + mysql_close(plain_conn); + + // Benchmark: ZSTD compressed connection (level 22 = max, high CPU cost) + MYSQL* zstd_conn = create_zstd_connection(cl, 22); + gettimeofday(&t0, NULL); + for (int i = 0; i < iterations; i++) { + mysql_query(zstd_conn, bench_query); + MYSQL_RES* r = mysql_store_result(zstd_conn); + mysql_free_result(r); } + gettimeofday(&t1, NULL); + double zstd_ms = elapsed_ms(t0, t1); + mysql_close(zstd_conn); + + diag(" %d iterations (cached result, no backend latency):", iterations); + diag(" No compression : %.1f ms", plain_ms); + diag(" ZSTD level 22 : %.1f ms", zstd_ms); + double ratio = zstd_ms / plain_ms; + diag(" Ratio (zstd/plain) : %.1fx", ratio); + + // Cleanup query rule + MYSQL_QUERY_T(admin, "DELETE FROM mysql_query_rules WHERE rule_id=1"); + MYSQL_QUERY_T(admin, "LOAD MYSQL QUERY RULES TO RUNTIME"); + + // ZSTD level 22 is extremely CPU-intensive. With cached results, + // the plain connection takes ~2s but ZSTD level 22 takes ~25s. + // If no compression were active, both would take ~2s. + // A ratio >= 3x proves compression is really running. + bool compression_active = (ratio >= 3.0); + ok(compression_active, + "ZSTD level 22 verified: %.1f ms vs %.1f ms (%.1fx slower = compression active)", + zstd_ms, plain_ms, ratio); + } + +#else + + // Test 10: mysql CLI ZSTD level 3 + if (mysql_supports_zstd) { + diag("Test 10: mysql CLI with --compression-algorithms=zstd --zstd-compression-level=3"); + diag(" Note: using mysql CLI because MariaDB connector 3.3.8 lacks ZSTD client support."); + MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=3"); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + const std::string name = std::string("-u") + cl.username; + const std::string pass = std::string("-p") + cl.password; + const std::string tg_port = std::string("-P") + std::to_string(cl.port); + + diag(" [CLI execvp] SELECT CONNECTION_ID() via ZSTD to port %s", tg_port.c_str()); + const std::vector id_args = { + "mysql", name.c_str(), pass.c_str(), "-h", cl.host, tg_port.c_str(), + "--compression-algorithms=zstd", "--zstd-compression-level=3", + "-e", "SELECT CONNECTION_ID() as conn_id" + }; + std::string id_result; + int id_res = execvp(mysql_client, id_args, id_result); + diag(" [CLI output] CONNECTION_ID:\n%s", id_result.c_str()); + + const char* large_query = + "SELECT t.THREAD_ID, t.PROCESSLIST_ID, t.PROCESSLIST_USER, t.PROCESSLIST_HOST, " + "t.NAME, sca.ATTR_NAME, sca.ATTR_VALUE " + "FROM performance_schema.threads t " + "JOIN performance_schema.session_connect_attrs sca " + "ON t.PROCESSLIST_ID = sca.PROCESSLIST_ID"; + diag(" [CLI execvp] Large resultset via new ZSTD connection"); + diag(" [CLI execvp] Query: %s", large_query); + const std::vector data_args = { + "mysql", name.c_str(), pass.c_str(), "-h", cl.host, tg_port.c_str(), + "--compression-algorithms=zstd", "--zstd-compression-level=3", + "-e", large_query + }; + std::string data_result; + int data_res = execvp(mysql_client, data_args, data_result); + size_t nl = 0; + for (char c : data_result) if (c == '\n') nl++; + diag(" [CLI output] %zu lines via ZSTD (level 3)", nl); + + ok(id_res == 0 && id_result.find("conn_id") != std::string::npos + && data_res == 0 && nl > 10, + "CLI ZSTD level 3: conn_id retrieved, %zu lines of data returned", nl); + } else { + skip(1, "mysql client does not support zstd compression"); + } + + // Test 11: mysql CLI ZSTD level 19 + if (mysql_supports_zstd) { + diag("Test 11: mysql CLI with --compression-algorithms=zstd --zstd-compression-level=19"); + MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=19"); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + const std::string name = std::string("-u") + cl.username; + const std::string pass = std::string("-p") + cl.password; + const std::string tg_port = std::string("-P") + std::to_string(cl.port); + + diag(" [CLI execvp] SELECT CONNECTION_ID() via ZSTD level 19"); + const std::vector id_args = { + "mysql", name.c_str(), pass.c_str(), "-h", cl.host, tg_port.c_str(), + "--compression-algorithms=zstd", "--zstd-compression-level=19", + "-e", "SELECT CONNECTION_ID() as conn_id" + }; + std::string id_result; + int id_res = execvp(mysql_client, id_args, id_result); + diag(" [CLI output] CONNECTION_ID:\n%s", id_result.c_str()); + + const char* large_query = + "SELECT t.THREAD_ID, t.PROCESSLIST_ID, t.PROCESSLIST_USER, t.PROCESSLIST_HOST, " + "t.NAME, sca.ATTR_NAME, sca.ATTR_VALUE " + "FROM performance_schema.threads t " + "JOIN performance_schema.session_connect_attrs sca " + "ON t.PROCESSLIST_ID = sca.PROCESSLIST_ID"; + diag(" [CLI execvp] Large resultset via new ZSTD connection at level 19"); + diag(" [CLI execvp] Query: %s", large_query); + const std::vector data_args = { + "mysql", name.c_str(), pass.c_str(), "-h", cl.host, tg_port.c_str(), + "--compression-algorithms=zstd", "--zstd-compression-level=19", + "-e", large_query + }; + std::string data_result; + int data_res = execvp(mysql_client, data_args, data_result); + size_t nl = 0; + for (char c : data_result) if (c == '\n') nl++; + diag(" [CLI output] %zu lines via ZSTD (level 19)", nl); + + ok(id_res == 0 && id_result.find("conn_id") != std::string::npos + && data_res == 0 && nl > 10, + "CLI ZSTD level 19: conn_id retrieved, %zu lines of data returned", nl); + } else { + skip(1, "mysql client does not support zstd compression"); } +#endif + // Restore defaults MYSQL_QUERY_T(admin, "SET mysql-zstd_compression_level=3"); MYSQL_QUERY_T(admin, "SET mysql-protocol_compression_level=3");