Skip to main content
Some extensions need to communicate with an external HTTP server or other network listener during tests — for example, an extension that makes outbound HTTP requests needs a local echo server to receive them. This page covers how to structure those tests so they run reliably under --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:
In the test, use MTR’s 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:
  1. Starts the server in a subprocess with start_new_session=True
  2. Blocks until the server has bound and written a readiness file
  3. Exits, allowing MTR to continue
MTR blocks on foreground --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 to echo_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:
Remove all temp files before the test ends. MTR’s check-testcase flags any file left in $MYSQLTEST_VARDIR/tmp/ as dirty state and fails the test:
For a complete, tested implementation of Pattern B see vsql-http/mysql-test/t/vsql_http_requests.test — it covers the full test lifecycle from --write_file through cleanup.

See also