--parallel=auto and across operating systems.
The patterns here apply equally to C++ and Rust extensions — both use MTR as the test runner via cargo vsql test.
Why hardcoded ports fail
Two failure modes make hardcoded ports unreliable in MTR: Port conflicts under--parallel. MTR assigns each worker a port range around its @@port value. A fixed port outside that range (say, 18777) may collide with another worker’s reserved block or with a system service — causing intermittent EADDRINUSE that only reproduces at high parallelism.
OS-level changes. New runner images and OS versions periodically reserve or restrict ports that previously worked. A port that passes locally and in CI today may silently fail to bind tomorrow.
The solution in both cases is the same: bind to port 0 and let the OS assign an available port, then communicate the actual port back to the test.
Pattern A: Server-integrated listener
Use this when the network listener runs inside the MySQL process — for example, an extension that embeds an HTTP server. In the extension, expose the bound port as a status variable after binding:wait_condition.inc to poll the status variable until it is non-zero, then capture the port into an MTR variable. Re-read after any UNINSTALL EXTENSION / INSTALL EXTENSION cycle — an ephemeral port changes on every bind.
Pattern B: External process listener
Use this when the listener is a standalone process — a Python helper server, a mock API, or any external program that MTR does not control directly.This pattern requires
python3 on PATH. GitHub-hosted runners include it; verify availability on self-hosted runners before using.The foreground launcher
Rather than backgrounding the server with--exec ... & and polling a port, use a foreground launcher script that:
- Starts the server in a subprocess with
start_new_session=True - Blocks until the server has bound and written a readiness file
- Exits, allowing MTR to continue
--exec calls, so this guarantees the server is ready before any SQL runs — no separate poller or sleep needed.
launcher.py — write this with --write_file:
echo_server.py — the actual server, writing its port before serving:
In the test
Start the launcher (foreground — MTR blocks until it exits), then source the port file:Keeping the port out of the result file
The result file records the SQL as written. If a URL contains the ephemeral port number, the result file would contain a different value on every run and the test would always fail on record/replay. Source the port file and set the MySQL session variable together under--disable_query_log so neither the let assignment nor the ephemeral port appears in the output. Use the session variable everywhere a URL is needed:
Process management
Log helper output — never discard it. The launcher pattern above writes the server’s stderr toecho_server.log and prints it on failure. Discarding helper output makes flakes unexplainable in CI artifacts.
Kill by PID, not by name. On Linux, pkill -f echo_server.py can match the MTR process itself via /proc/cmdline. Use the PID file written by the launcher:
$MYSQLTEST_VARDIR/tmp/ as dirty state and fails the test:
vsql-http/mysql-test/t/vsql_http_requests.test — it covers the full test lifecycle from --write_file through cleanup.
See also
- C++ Testing — MTR setup, running suites, recording results, and debugging failures
- Creating Extensions in C++ — end-to-end build steps and CMake setup
- C++ Development — VDF authoring, argument types, and result handling

