test: expand zstd_compression_level test with MySQL 8.4 C API and timing benchmark

The existing test only verified admin variable behavior (default, range,
independence). It did not prove that ZSTD compression was actually
active on the wire. Two new test variants address this:

MariaDB connector path (mysql-zstd_compression_level-t):
- Tests 1-9: unchanged (variable defaults, range validation, independence)
- Tests 10-11: use mysql CLI via execvp with --compression-algorithms=zstd
  and --zstd-compression-level={3,19}. Each call spawns a new process
  (= new connection). Verifies CONNECTION_ID() retrieval and large
  resultset transfer. Skipped if mysql CLI lacks zstd support.

MySQL 8.4 connector path (mysql-zstd_compression_level_libmysql-t):
- Tests 1-9: same as MariaDB path
- Test 10: establishes a ZSTD connection using mysql_options(
  MYSQL_OPT_COMPRESSION_ALGORITHMS, "zstd") and prints mysql_thread_id()
  without running any query.
- Test 11: timing benchmark proving compression is active. Creates a
  query rule (rule_id=1, cache_ttl=60000) to cache all SELECTs, then
  runs a 576-row resultset query 1000 times on a plain connection and
  1000 times on a ZSTD level 22 connection. ZSTD level 22 is extremely
  CPU-intensive: observed 40122ms vs 858ms (46.7x ratio). A ratio >= 3x
  proves compression is really running on the client<->ProxySQL link.

Other changes:
- MySQL_Protocol.cpp: simplify redundant clamping to a simple cast
  (the variable is already validated at SET time)
- Makefile: add build rule for _libmysql variant linking against
  MySQL 8.4 connector
- groups.json: register _libmysql variant for mysql84/90/95-g1
- CLAUDE.md: update testing section with CI script usage and DO NOT list
v3.0_new_zstd
Rene Cannao 1 month ago
parent f9f6f74ecc
commit 174c5bda20

@ -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 `<infra>-g<N>` (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 <testname>-t` compiles `<testname>-t.cpp` into `<testname>-t`. No special Makefile target is needed for new tests — just add the `.cpp` file and register it in `groups.json`.
## Architecture
### Build Pipeline

@ -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<uint8_t>(std::min<int>(ZSTD_maxCLevel(), std::max<int>(1, mysql_thread___zstd_compression_level)));
: static_cast<uint8_t>(mysql_thread___zstd_compression_level);
myconn->options.compression_zstd = false;
myconn->options.zstd_compression_level = 0;

@ -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" ],

@ -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 $@

@ -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 <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <sys/time.h>
#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<const char*> 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<const char*> 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<const char*> 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<const char*> 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");

Loading…
Cancel
Save