You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/doc/plugin-chassis/FILE_CHANGES.md

511 lines
45 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Plugin Chassis (PR #5651) — File-by-File Inventory
This document catalogues every file added or modified by PR #5651 (branch `plugin-chassis` at HEAD `55e90d1a7` vs base `origin/v3.0` at `6ef036a00`). It's the appendix to [`REVIEW_GUIDE.md`](./REVIEW_GUIDE.md) — use this when you've identified a file in a diff and want to know what it does without reading the full file.
**Scope:** 105 files changed (91 NEW + 14 MODIFIED), +48,749 / 32 lines. Of those:
- ~23,000 lines are mechanically generated protobuf C++ under `plugins/mysqlx/proto/` — described once in §M, not file-by-file.
- ~5,600 lines of handwritten C++ in `plugins/mysqlx/src/` (the X Protocol plugin proper).
- ~2,200 lines of handwritten C++ in `lib/` and `include/` (the chassis core).
- ~12,000 lines of test code under `test/tap/tests/unit/` and `test/tap/tests/`.
Sections **AG** cover the **chassis core**. Sections **HO** cover the **mysqlx plugin**. Sections **PT** cover **tests, CI, infra, docs, and build glue**. Each file row carries: path, status, line-count delta, purpose, key contents, what to spot-check.
---
## A. Chassis: public ABI headers (the contract)
### `include/ProxySQL_Plugin.h` — NEW, 324 lines
- **Purpose:** the C++ ABI a plugin compiles against; all types are file-wide guarded by `#ifdef PROXYSQL40` so v3.x TUs see nothing.
- **Key contents:**
- `PROXYSQL_PLUGIN_ABI_VERSION` (currently `3`) and `_MAX` (also 3). ABI 1 = original 6-field descriptor; ABI 2 appends `register_schemas` for the four-phase lifecycle; ABI 3 keeps the descriptor layout unchanged from ABI 2 and adds a single `register_runtime_view` callback at the **tail of `ProxySQL_PluginServices`** so plugins can declare admin-side projections of module state. ABI-2 plugins still load on an ABI-3 core (loader range is `[1, 3]`; the trailing services field is invisible to the older compile).
- `ProxySQL_PluginDescriptor` — 7-field struct `{name, abi_version, init, start, stop, status_json, register_schemas}` returned via `extern "C" proxysql_plugin_descriptor_v1()`.
- `ProxySQL_PluginServices` — services injected into the plugin: `register_table`, `register_command`, `register_command_alias`, three snapshot stubs (always nullptr today), `log_message`, three `get_*db` getters, `register_query_hook`, `get_prometheus_registry`, `register_runtime_view` (ABI 3, tail-appended). Tail-append discipline.
- Per-phase availability matrix is documented inline above the struct (Phase B: `register_table` live, DB getters return nullptr; Phase D: full services).
- Query-hook ABI — `ProxySQL_PluginQueryHookPayload/Result/Action`; one hook per protocol per plugin.
- Runtime-view ABI — `ProxySQL_PluginRuntimeView{table_name, refresh, opaque}` plus `proxysql_plugin_register_runtime_view_cb`. The refresh callback gets a borrowed `SQLite3DB*` and re-projects module state into the named admin-db table on demand.
- Header explicitly calls out the `std::string` / `prometheus-cpp` C++-ABI coupling: plugins MUST share toolchain with core.
- Footer comment block encodes the **separation-of-duties contract**: LOAD reads the editable admin table and hands rows to the module's typed install API (never touches `runtime_<X>`); SAVE dumps module state back to the editable table; `runtime_<X>` is an admin-side projection refreshed by the registered view callback. Disk-tier copies are still plain BEGIN/DELETE/INSERT/COMMIT and still subject to the empty-source-must-clear-destination rule (PR #5643).
- **Spot-check:**
- Verify `register_schemas` is only dereferenced when `abi_version >= 2`.
- Verify `register_runtime_view` lives at the END of `ProxySQL_PluginServices`; older plugins compiled against the ABI-2 layout never see the field.
- Verify the loader rejects `abi_version > PROXYSQL_PLUGIN_ABI_VERSION_MAX` rather than reading past its struct.
### `include/ProxySQL_PluginManager.h` — NEW, 172 lines
- **Purpose:** declares the in-process loader/dispatcher (`ProxySQL_PluginManager`) plus the free-standing helpers main.cpp/Admin call.
- **Key contents:**
- Class is move-only-ish (copy ctor/assign deleted).
- Public lifecycle: `load`, `invoke_register_schemas_phase`, `init_all`, `start_all`, `stop_all`; plus `tables(kind)`, `dispatch_admin_command`, `register_command_alias`, `resolve_alias_to_canonical`, `register_query_hook`, `has_query_hook`, `dispatch_query_hook`, `register_runtime_view`, `refresh_runtime_views_for_query`.
- Free-standing API: `proxysql_get_plugin_manager` (atomic load), `proxysql_load_configured_plugins` (Phase A+B, **publishes** the manager), `proxysql_init_configured_plugins` (Phase D), `proxysql_start_configured_plugins`, `proxysql_stop_configured_plugins`, plus three dispatch helpers (`dispatch_configured_plugin_admin_command`, `dispatch_configured_plugin_query_hook`, lock-free `has_configured_plugin_query_hook`), `resolve_configured_plugin_admin_alias`, and `proxysql_refresh_configured_plugin_runtime_views` (admin pre-SELECT hook).
- Internal `plugin_handle_t` carries dlopen handle + `schemas_registered/initialized/started/stopped` state flags. `runtime_views_` vector holds `registered_runtime_view_t{table_name, refresh, opaque}` entries.
- Two services structs: `services_` (Phase D, full services) and `services_phase_b_` (Phase B, with stubbed DB getters and stubbed `register_query_hook`). **`register_runtime_view` is wired in BOTH structs** — plugins typically declare runtime views alongside their tables in `register_schemas`, so the callback is live in Phase B as well as Phase D.
- **Spot-check:**
- Lifecycle invariant: `proxysql_init_configured_plugins` must run BEFORE any worker thread takes the lock-free read path.
- `resolve_alias_to_canonical` returns `std::string` by value (not borrowed `c_str()`) so a concurrent reload between resolve and dispatch can't dangle a pointer.
---
## B. Chassis: loader implementation
### `lib/ProxySQL_PluginManager.cpp` — NEW, 1,038 lines, file-wide gated under `PROXYSQL40`
- **Purpose:** dlopen orchestration, services trampolines, lifecycle state machine, dispatch.
- **Key contents:**
- **Concurrency model:** `g_active_plugin_manager` is a `std::atomic<ProxySQL_PluginManager*>`; reads/writes coordinated via `g_active_plugin_manager_mutex` (`std::shared_mutex` — readers from dispatch/resolve paths share, publishers/unpublishers take unique). A separate `std::mutex g_plugin_lifecycle_mutex` serializes load/start/stop transitions. The shared-mutex change exists explicitly to keep query-hook dispatch from collapsing per-worker thread parallelism.
- **Service trampolines** (file-static): `register_table_service`, `register_command_service`, `register_command_alias_service`, `register_query_hook_service`, `register_runtime_view_service`, `get_admindb/configdb/statsdb_service`, `log_message_service`, `get_prometheus_registry_service`. Each writes to `g_registry_target` set by a `ScopedRegistryTarget` RAII guard around plugin callbacks; `note_registration_failure` records the first failure.
- **`sql_references_table_ci()`** is the matcher used by `refresh_runtime_views_for_query`: case-insensitive whole-identifier substring match treating `[A-Za-z0-9_]` as identifier characters. So `runtime_mysqlx_users` matches in `` SELECT * FROM `runtime_mysqlx_users` `` but NOT in `runtime_mysqlx_users_extra` or `stats_runtime_mysqlx_users`.
- **`load()`** (lines 324383): rejects duplicate paths; `dlopen(RTLD_NOW|RTLD_LOCAL)`; resolves `proxysql_plugin_descriptor_v1`; rejects null/empty `name` and `abi_version` outside `[1, PROXYSQL_PLUGIN_ABI_VERSION_MAX]`.
- **`invoke_register_schemas_phase()`** (lines 386461): Phase B; only reads `descriptor->register_schemas` when `abi_version >= 2`; snapshots `tables_*`/`commands_`/storage sizes and rolls back on failure to keep retries idempotent.
- **`init_all()`** has the same snapshot/rollback contract.
- **`stop_all()`** (lines 548576): pairs `stop()` with `init()`, NOT with `start()` — every plugin whose init succeeded gets stop, even if its own start failed (teardown symmetry to avoid leaks). Plugins are marked `stopped=true` even on failure; idempotent across destructor.
- **`canonicalize_plugin_command()`**: trims, strips trailing `;`, collapses internal whitespace.
- **Alias collision rules** in `register_command_alias` (lines 765809): rejects shadowing another command's canonical or alias; idempotent for duplicates.
- **Publish ordering** in `proxysql_load_configured_plugins` (lines 896963): manager is installed as active **before** Phase D so `ProxySQL_Admin::init` can read tables via `proxysql_get_plugin_manager`. Comments call out the unsafe-reordering hazard.
- **`stop_configured_plugins`** clears the active pointer before calling `stop_all`, then `manager.reset()` always runs (so the `.so` is unmapped even if a plugin's stop returned false).
- **Spot-check:**
- Confirm the `descriptor->register_schemas` access is gated behind the `abi_version >= 2u` check.
- Verify the `RTLD_LOCAL` flag — no plugin should pollute the global symbol table.
- Confirm `proxysql_dispatch_configured_plugin_query_hook` calls `g_active_plugin_manager.load()` AFTER taking the shared lock; the lock-free `proxysql_has_configured_plugin_query_hook` is documented to allow false positives.
---
## C. Chassis: admin integration
### `lib/Admin_Bootstrap.cpp` — MODIFIED, +57 lines
- **Purpose:** merges plugin-declared schemas into `tables_defs_{admin,config,stats}` so the existing `check_and_build_standard_tables` DDL pass materializes them.
- **Key contents:**
- `ProxySQL_Admin::init()` consults `proxysql_get_plugin_manager()`, walks each `ProxySQL_PluginDBKind`, dedup-checks against existing tables, and appends via `insert_into_tables_defs`. On a name conflict it logs and returns false — the caller (now wired to exit) treats this as fatal.
- Trailing comment block (lines ~1346) explains why `materialize_plugin_tables()` was deleted: it was a post-init no-op since the merge already happens here.
- **Spot-check:**
- Verify `init()` returns false on the conflict path AND that `src/main.cpp` actually checks the return value.
- Note the indentation churn around `#ifdef PROXYSQLGENAI` — review for accidental scope changes.
### `lib/Admin_Handler.cpp` — MODIFIED, +43 lines
- **Purpose:** generic dispatch path replacing the previous hard-coded MYSQLX alias ladder.
- **Key contents:**
- `admin_session_handler()` calls `proxysql_resolve_configured_plugin_admin_alias(query_str)` early; if a canonical comes back, the handler invokes `SPA->dispatch_plugin_admin_command(sess, plugin_canonical.c_str())` and short-circuits.
- On the rare race where resolve found a command but dispatch returned false (plugin uninstalled between calls), responds with "Plugin failed to handle registered command".
- **Spot-check:**
- Confirm `plugin_canonical` is held by value (`std::string`), not as a raw pointer.
- Verify the new branch sits BEFORE the generic LOAD/SAVE handler.
### `lib/ProxySQL_Admin.cpp` — MODIFIED, +42 lines
- **Purpose:** exposes admin/config/stats DB handles to the loader, adds the templated dispatcher, and wires the pre-SELECT runtime-view dispatch.
- **Key contents:**
- Three free functions `proxysql_plugin_get_admindb/configdb/statsdb()` return `GloAdmin->{admindb,configdb,statsdb}` if `GloAdmin` is non-null. Gated by `PROXYSQL40` so v3.x exports no plugin-aware symbols at all.
- `ProxySQL_Admin::dispatch_plugin_admin_command<S>(sess, sql)` builds a `ProxySQL_PluginCommandContext{admindb,configdb,statsdb}`, calls `proxysql_dispatch_configured_plugin_admin_command`, and translates `result` into `send_ok_msg_to_client` / `send_error_msg_to_client`.
- Explicit template instantiations for `MySQL_Session` and `PgSQL_Session`.
- **Pre-SELECT runtime-view dispatch in `GenericRefreshStatistics`** (around line 1654): `proxysql_refresh_configured_plugin_runtime_views(query_no_space, admindb)` is called for **every admin-port query**, gated only on `if (admin)` and placed **outside** the existing `if (refresh==true)` block. The chassis itself decides whether any plugin's refresh callback fires, by case-insensitive whole-identifier substring match against the query (matching the wording used in section B for the `sql_references_table_ci` matcher) — a query that touches no registered view is a cheap no-op (one shared lock + N substring scans). The `refresh` flag is left untouched; it gates a separate set of core-only refreshes (stats_mysql_processlist, runtime_mysql_users, etc.).
### `include/proxysql_admin.h` — MODIFIED, +8 lines
Declares the templated dispatcher and pulls `ProxySQL_Plugin.h`. Gating matches the cpp instantiation gate.
---
## D. Chassis: startup wiring
### `src/main.cpp` — MODIFIED, +73 lines
- **Purpose:** owns the `GloPluginManager` unique_ptr and threads it through `ProxySQL_Main_init_phase2`.
- **Key contents:**
- `static std::unique_ptr<ProxySQL_PluginManager> GloPluginManager;` (file scope).
- Four wrapper functions: `LoadConfiguredPlugins`, `InitConfiguredPlugins`, `StartConfiguredPlugins`, `StopConfiguredPlugins`. Each `exit(EXIT_FAILURE)` on error during startup; shutdown only logs.
- `UnloadPlugins()` now calls `StopConfiguredPlugins` first, before tearing down the legacy plugin .sos.
- Ordering in `ProxySQL_Main_init_phase2___not_started`: `LoadConfiguredPlugins``ProxySQL_Main_init_Admin_module``InitConfiguredPlugins``StartConfiguredPlugins`.
- `ProxySQL_Main_init_Admin_module` now checks `GloAdmin->init()` return and exits on failure.
- Config parsing: `proxysql_load_plugin_modules_from_config(root, GloVars.plugin_modules)` from the libconfig `Setting&`.
- **Spot-check:**
- Phase-D-before-workers ordering: confirm `Init/StartConfiguredPlugins` complete before the worker-thread spawning later in `ProxySQL_Main_init_phase3___start_all`.
- Verify `GloPluginManager` is at file scope, not function-local — destructor needs to run during process teardown.
### `include/proxysql_glovars.hpp` and `lib/ProxySQL_GloVars.cpp` — MODIFIED, +25 / +26 lines
- **Purpose:** add the `plugin_modules` `std::vector<std::string>` to GloVars and the libconfig parser helper.
- **Key contents:**
- Header gains a forward decl of `_debug_level` (works around a circular include when plugin unit tests pull in `proxysql_glovars.hpp` directly without `proxysql_structs.h`).
- Free function `proxysql_load_plugin_modules_from_config(const Setting& root, std::vector<std::string>& plugin_modules)`: clears, reads `plugins` setting, pushes each string entry. Silently skips non-string entries.
- Constructor and destructor explicitly clear `plugin_modules`.
---
## E. Chassis: build system
### `Makefile` (top-level) — MODIFIED, +41 / 14 lines
- **Purpose:** introduces `PROXYSQL40` tier, propagates the macro and runs the mysqlx plugin sub-build.
- **Key contents:**
- Tier hierarchy: `PROXYSQLGENAI``PROXYSQL40``PROXYSQL31`.
- `PROXYSQL40=1` triggers a major-version bump (3.0.x → 4.0.x) via the same awk recipe.
- Each `build_src_*` target conditionally `cd plugins/mysqlx && $(MAKE)` (skipped if `PROXYSQL40` unset).
- `clean*` targets descend into `plugins/mysqlx`. Top-level `cleanbuild` recurses unconditionally; the plugin Makefile gates its protobuf check to avoid breaking on `clean` (commit `9f5ed235b`).
- `install`/`uninstall` add `/usr/lib/proxysql/plugins/ProxySQL_MySQLX_Plugin.so`.
### `lib/Makefile` and `src/Makefile` — MODIFIED, +8 / +7 lines
Wire `PSQL40` into compile flags; add `ProxySQL_PluginManager.oo` to `_OBJ_CXX`.
---
## F. Chassis: documentation
### `doc/PLUGIN_API.md` — NEW, 470 lines
Reference for plugin authors; covers loading, four-phase startup, descriptor contract, services, ABI versioning, the empty-source-sync invariant. **For PR-reviewer documentation, see [`REVIEW_GUIDE.md`](./REVIEW_GUIDE.md) and [`ABI.md`](./ABI.md).**
---
## G. Chassis: test scaffolding
### `test/tap/test_helpers/fake_plugin.cpp` — NEW, 280 lines
The harness plugin source; one .cpp builds two .sos (`fake_plugin` and `fake_plugin2`) via `-DFAKE_PLUGIN_NAME=` / `-DFAKE_PLUGIN_ENV_PREFIX=`.
- Three descriptors exposed: `fake_descriptor` (ABI 1, 6 fields), `fake_descriptor_with_phase_b` (ABI 2), `fake_descriptor_bogus_abi` (version 99 — exercises rejection).
- Behaviour toggles via env vars (`PROXYSQL_FAKE_PLUGIN_*`): `INIT_FAIL`, `START_FAIL`, `STOP_FAIL`, `FORCE_BOGUS_ABI`, `ENABLE_PHASE_B`, `_PHASE_B_FAIL`, `_PHASE_B_REGISTER_TABLE`, `_PHASE_B_TOUCH_HANDLES`, `_PHASE_B_PARTIAL_THEN_FAIL`, `HOOK_DENY`. Each emits a log line so tests can assert lifecycle ordering.
- `fake_query_hook` echoes `payload.query_text` back; deny vs allow controlled by env.
---
## H. Mysqlx plugin: entry point
### `plugins/mysqlx/include/mysqlx_plugin.h` — NEW, 74 lines
- Public surface of the plugin — declares `MysqlxPluginContext` and the listener-reconcile entry points.
- `MysqlxPluginContext` holds `services` pointer (chassis ABI), `unique_ptr<MysqlxConfigStore>`, `vector<unique_ptr<Mysqlx_Thread>>`, `route_to_thread` map + mutex + `next_rr_index` for RR thread assignment.
- `mysqlx_reconcile_listeners()` is `__attribute__((weak))` so unit tests linking just admin_schema.cpp resolve cleanly; production .so always has the strong def.
- `mysqlx_reconcile_listeners_impl()` is the pure variant for tests.
### `plugins/mysqlx/src/mysqlx_plugin.cpp` — NEW, 270 lines
- Implements the `ProxySQL_PluginDescriptor` entry exported via `proxysql_plugin_descriptor_v1()`.
- Four-phase lifecycle: `mysqlx_register_schemas` (Phase B, services without DB), `mysqlx_init` (Phase D, DB live), `mysqlx_start`, `mysqlx_stop`.
- `replace_table_atomically()` — every `admindb.execute()` return is checked; defensive ROLLBACK on any failure including post-COMMIT, addresses the v3.0 silent-wipe bug.
- `mysqlx_start()` does `sync_disk_to_memory()` (configdb → editable mysqlx_* tables) then four `install_<X>_from_admin` calls to populate the in-memory `MysqlxConfigStore` directly from the editable admin tables — no `runtime_mysqlx_*` write happens here, since runtime tables are now admin-side projections, not a tier the plugin maintains. Then clamps pool size 1..64 and drives `mysqlx_reconcile_listeners()` on the same path used by `LOAD MYSQLX ROUTES TO RUNTIME`.
- `parse_bind_addr()` handles both `host:port` and `[ipv6]:port`; default port 33060.
- Descriptor is constexpr-ish and pinned to `PROXYSQL_PLUGIN_ABI_VERSION`.
---
## I. Mysqlx plugin: admin schema integration
### `plugins/mysqlx/include/mysqlx_admin_schema.h` — NEW, 8 lines
Single-symbol header declaring `mysqlx_register_admin_schema()`.
### `plugins/mysqlx/src/mysqlx_admin_schema.cpp` — NEW
- Registers DDL for all `mysqlx_*` tables and the `LOAD/SAVE` admin commands.
- Four config-table pairs (memory + runtime): `mysqlx_users`, `mysqlx_routes`, `mysqlx_backend_endpoints`, `mysqlx_variables`, all with `JSON_VALID(attributes)` CHECK constraints.
- Two stats-only tables: `stats_mysqlx_routes`, `stats_mysqlx_processlist`.
- 8 LOAD/SAVE TO RUNTIME commands plus 8 disk variants; each has alias group (`FROM MEMORY`, `FROM MEM`, `TO RUN`, etc.) registered via `register_command_alias`.
- LOAD/SAVE callbacks no longer copy between admin tables. Each `load_<X>_to_runtime` callback invokes `MysqlxConfigStore::install_<X>_from_admin(admindb, err)` (read editable mysqlx_<X>, swap into the module's in-memory state under its own lock). Each `save_<X>_from_runtime` callback invokes `MysqlxConfigStore::save_<X>_to_admin_table(admindb)` (dump module state into the editable mysqlx_<X> table). Disk-tier copies (LOAD/SAVE FROM/TO DISK) remain plain BEGIN/DELETE/INSERT/COMMIT between configdb and admindb.
- Four free functions `refresh_users_runtime_view` / `refresh_routes_runtime_view` / `refresh_endpoints_runtime_view` / `refresh_variables_runtime_view` are registered at schema-registration time via `services.register_runtime_view({"runtime_mysqlx_<X>", &refresh_<X>_runtime_view, nullptr})`. Each calls `MysqlxConfigStore::project_<X>_to_runtime_view(admindb)` to wipe the destination admin-db table and refill it from the module's in-memory state. The chassis fires the relevant callback before any admin SELECT against the projected table.
- `load_routes_to_runtime` calls `mysqlx_reconcile_listeners` weak-pointer if non-null.
---
## J. Mysqlx plugin: configuration runtime
### `plugins/mysqlx/include/mysqlx_config_store.h` — NEW
### `plugins/mysqlx/src/mysqlx_config_store.cpp` — NEW
- **Authoritative in-memory source of truth** for X-protocol routing/identity state. Sessions/threads consume it directly; the `runtime_mysqlx_*` tables in admin_db are admin-side projections of this store, refilled on demand by the plugin's runtime-view refresh callbacks (see §I).
- `MysqlxResolvedIdentity` (was `MysqlxCredentials` — renamed in `84bbdfdca`); now carries a `comment` field.
- `MysqlxRoute` also carries a `comment` field.
- `MysqlxBackendAuthMode` enum: `mapped` / `service_account` / `pass_through`.
- Public struct `MysqlxBackendEndpointOverride{hostname, mysql_port, mysqlx_port, use_ssl, attributes, comment}` — promoted from a file-local `MysqlxEndpointOverride` so SAVE / projection can round-trip the per-(hostname, mysql_port) overrides.
- API: per-entity install / save / project triplets — `install_users_from_admin(db, err)` / `save_users_to_admin_table(db)` / `project_users_to_runtime_view(db)`, same shape for routes / endpoints / variables; convenience `install_all_from_admin(db, err)` (test-only fixture helper); `snapshot_active_routes()` returning `vector<pair<name, bind>>` for the listener reconciler; plus the read-side `resolve_identity`, `pick_endpoint`, `route_hostgroup`, `route_exists`.
- `mutable std::shared_mutex mutex_` — readers (resolve_identity, pick_endpoint, route_exists, snapshot_active_routes, project_*_to_runtime_view) take shared locks; install_*_from_admin takes exclusive. Each install is independent: LOAD MYSQLX USERS does not touch routes/endpoints/variables.
- `install_users_from_admin()` reads the editable `mysqlx_users` plus cross-module `runtime_mysql_users` (canonical password/hostgroup); `install_endpoints_from_admin()` reads the editable `mysqlx_backend_endpoints` and cross-module `runtime_mysql_servers WHERE status='ONLINE'`. None of the install paths touch any `runtime_mysqlx_*` table — those are projections, not inputs.
- `endpoint_overrides_` (the public-struct overrides) is preserved across install_endpoints calls so SAVE can round-trip and the runtime-view projection faithfully reflects what was loaded.
- `pick_from_hostgroup()` supports `first_available` and `round_robin[_with_fallback]`; round-robin uses a separate `rr_mutex_` so RR state doesn't interfere with config swaps.
- Test-only `install_for_test` is unconditionally available — note it bypasses `install_*_from_admin`.
---
## K. Mysqlx plugin: listener reconciliation
### `plugins/mysqlx/src/mysqlx_listener_reconcile.cpp` — NEW
- Desired-state reconciler from the `MysqlxConfigStore` (the authoritative in-memory state) to per-thread listener fds. Signature is `mysqlx_reconcile_listeners_impl(const MysqlxConfigStore& store, ...)` — no DB handle.
- 3-step algorithm: snapshot desired set → remove stale/mis-bound listeners → add missing listeners (RR over thread pool).
- Bind-change detection compares `threads[tidx]->get_listener_addr_for_route()` to `host:port` string; mismatch is treated as remove+add.
- Single-admin-thread assumption is documented inline; the desired set comes from `store.snapshot_active_routes()` (taken under the store's shared lock, dropped before listener fd manipulation) rather than a SELECT against `runtime_mysqlx_routes`. An inline comment calls out why we deliberately do NOT read `runtime_mysqlx_routes` here: that table is an on-demand admin-side projection of the store, only populated when admin runs a SELECT against it; reading it from the LOAD path would see empty/stale data on every startup and every `LOAD MYSQLX ROUTES TO RUNTIME` call.
- RR cursor `next_rr_index` is wrapped via `((next_rr_index % pool) + pool) % pool` — defensive against negative.
- **Note:** if `add_listener` fails (port in use), the route is silently not added to `route_to_thread`; operator gets no error feedback beyond the chassis-level log. Future enhancement opportunity.
---
## L. Mysqlx plugin: protocol layer
### `plugins/mysqlx/include/mysqlx_protocol.h` — NEW, 65 lines
### `plugins/mysqlx/src/mysqlx_protocol.cpp` — NEW, 298 lines
- Frame-level helpers — `MysqlxFrameHeader` (4-byte LE size + 1-byte type), 16 MB cap, encode/decode, blocking `read_exact` / `write_all`, `send_error`/`send_ok` over a raw fd, hex helpers, MYSQL41 SHA1 scramble/verify routines.
- SHA1 via OpenSSL EVP API (3.0+ clean).
- MYSQL41 implementation: `stage1=SHA1(pw)`, `stage2=SHA1(stage1)`, `scramble=stage1 XOR SHA1(challenge||stage2)`. Verifier uses `CRYPTO_memcmp` (constant-time).
- `mysqlx_mysql41_verify_hash()` works against a stored stage2 hash (server-side compute) — important since plugin only stores hash, not plaintext.
- `mysqlx_build_frame()` rejects payloads >= `MYSQLX_MAX_PAYLOAD_SIZE` so the +1 cannot wrap.
- `mysqlx_send_error/ok` use Mysqlx::Error / Mysqlx::Ok protobufs; these are the sync (blocking) helpers used outside the data-plane (init paths, fatal frames).
---
## M. Mysqlx plugin: data stream
### `plugins/mysqlx/include/mysqlx_data_stream.h` — NEW, 138 lines
### `plugins/mysqlx/src/mysqlx_data_stream.cpp` — NEW, 373 lines
- Buffered I/O with optional in-memory BIO-wrapped SSL: `read_buf_/write_buf_`, deque of complete `MysqlxFrame`s, `ssl_write_buf_` for outbound encrypted bytes, `parse_error_` flag, `poll_fds_idx`, status enum.
- `init()` sets non-blocking; `close_and_reset()` tears SSL + clears everything; `clear_io_buffers()` (added in commit `4bd4b462b`) scrubs only buffers and parse-error, preserving SSL/BIO/encrypted state — used by `MysqlxConnection::reset()` between pool reuses.
- `try_parse_frame()` validates `1 <= payload_size <= 16 MB`, sets `parse_error_=true` on out-of-range, slides read window after >4 KiB consumed.
- SSL path uses memory BIOs: `read_from_net()` pumps `recv → BIO_write(rbio_ssl_)`, then `SSL_read` into `feed_bytes`. `write_to_net()` `SSL_write`s app data, then `queue_encrypted_output()` via `BIO_read(wbio_ssl_)` and `flush_ssl_write_buf()`.
- `do_ssl_handshake()` post-handshake drain: **64 KiB `static thread_local` scratch** (commit `55e90d1a7`) — avoids the worker stack on ASan / large-pool builds.
- SSL_read returning 0 surfaced as connection close (close_notify) rather than WANT_IO.
---
## N. Mysqlx plugin: backend connection
### `plugins/mysqlx/include/mysqlx_connection.h` — NEW, 135 lines
### `plugins/mysqlx/src/mysqlx_connection.cpp` — NEW, 318 lines
- Pooled outbound connection holding fd, hostgroup, user/schema, transaction/prepared-stmt flags, the backend `MysqlxDataStream`, optional `SSL_CTX*`, and a 10-state `BackendAuthState` enum.
- `start_connect()` uses `inet_pton(AF_INET, host, ...)` and **rejects non-IPv4-literal hostnames** (commit `55e90d1a7` — previously the return was discarded and a hostname silently became 0.0.0.0).
- `check_connect()` polls with timeout vs `connect_timeout_ms_`, checks `SO_ERROR`.
- Backend auth state machine: capabilities GET → CapabilitiesSet (advertise MYSQL41 + optional `tls=true`) → optional TLS handshake → AuthenticateStart → AuthenticateContinue (compute scramble) → AuthenticateOk.
- `read_auth_frame()` filters out leading NOTICE frames (backends emit session-state-change notices); previously caused the auth state machine to spin to handshake timeout.
- `step_auth_authenticate_start_sent()`: **`ParseFromArray` return is now checked** (commit `4bd4b462b`); previously a malformed AuthenticateContinue produced an undefined-input scramble.
- `reset()` calls `backend_ds_.clear_io_buffers()` to scrub straggler frames between pool checkouts while preserving the SSL session.
---
## O. Mysqlx plugin: session state machine — the core
### `plugins/mysqlx/include/mysqlx_session.h` — NEW, 301 lines
- Per-client session state. The big enum is `MysqlxSession::Status` (21 values).
- `Status` enum drives `handler()`. Compression negotiation state: `MysqlxCompressionAlgo` (NONE / ZSTD_STREAM / LZ4_MESSAGE), combine-mixed flag, max-combine count.
- Lazy zstd contexts (`ZSTD_DCtx*`, `ZSTD_CCtx*`) opaquely forward-declared; only the .cpp pulls `<zstd.h>`.
- Test-only forgery setters (`inject_identity_for_test`, `resolve_backend_target_for_test`) are gated behind `MYSQLX_TEST_BUILD` (commit `04bccec51`); read-only `target_*_for_test` getters remain unconditional.
- `MysqlxTlsMode`: only `TLS_OFF` / `TLS_TERMINATE` remain — the dead `TLS_PASSTHROUGH` was removed in `55e90d1a7` (it never set up a real opaque pipe).
### `plugins/mysqlx/src/mysqlx_session.cpp` — NEW, 1,604 lines (the biggest file)
State → handler mapping (from `handler()` switch at line 187):
| State | Handler | Role |
|---|---|---|
| `CONNECTING_CLIENT` | `handler_connecting_client()` (L230) | initial frame intake; routes to capabilities or auth |
| `X_CAPABILITIES_GET` | `handler_capabilities_get()` (L265) | emits supported caps incl. tls + compression list |
| `X_CAPABILITIES_SET` | `handler_capabilities_set()` (L352) | parses CapabilitiesSet; **5051/5052 errors on malformed body** (commit `4bd4b462b`); two-pass for compression vs tls |
| `X_AUTH_START` | `handler_auth_start()` (L520) | dispatches PLAIN vs MYSQL41 |
| `X_AUTH_CHALLENGE_SENT` | `handler_auth_challenge_response()` (L561) | verifies MYSQL41 scramble, then `resolve_backend_target()` before send_auth_ok |
| `WAITING_CLIENT_XMSG` | `handler_waiting_client_msg()` (L724) | data-plane client → backend pump; calls `dispatch_client_message()` (L653) which intercepts Compression frames |
| `CONNECTING_SERVER` | `handler_connecting_server()` (L1028) | drives backend connect + auth state machine |
| `WAITING_SERVER_XMSG` | `handler_waiting_server_msg()` (L801) | backend → client pump; routes through `send_to_client_compressed()` |
| `X_TLS_ACCEPT_INIT` | `handler_tls_accept_init()` (L918) | drives client-side `do_ssl_handshake()` |
| `X_SESSION_RESET_WAITING` | `handler_session_reset_waiting()` (L856) | observes terminal frame for SessionReset |
| `X_SESSION_CLOSING` | `handler_session_closing()` (L912) | drains and marks unhealthy |
States `X_AUTH_OK_SENT`, `X_AUTH_FAILED`, `X_TLS_ACCEPT_CONT/DONE`, `X_TLS_CONNECT_*`, `X_SESSION_CLOSED`, `PROCESSING_X_QUERY` are reachable as transient or sentinel values but have no dedicated handler in the dispatch.
Other notable members:
- `resolve_backend_target()` (L956) — error codes 4000 (empty default_route), 4001 (unknown route), 4002 (no endpoints); records `mysqlx_stats().record_conn_err()`.
- `shutdown_notify_client()` (L1143) — commit `55e90d1a7`: enqueues 1053 fatal Error, single-pass `write_to_net()`, `SSL_set_quiet_shutdown(1)+SSL_shutdown`, then `status=X_SESSION_CLOSING`. Idempotent and skipped if fd<0 or already closing.
- Compression Phase 1: negotiation in `handler_capabilities_set`; Phase 2: `handle_compression_message()` (L1292); Phase 3: `send_to_client_compressed()` (L1576), `emit_single_compressed()`, `emit_batched_compressed()`, `flush_compression_batch()`.
---
## P. Mysqlx plugin: worker thread
### `plugins/mysqlx/include/mysqlx_thread.h` — NEW, 123 lines
### `plugins/mysqlx/src/mysqlx_thread.cpp` — NEW, 419 lines
- Owns listener fds, accept loop, poll set, sessions, per-thread connection cache.
- Parallel listener vectors (`listener_fds_`, `listener_addrs_`, `listener_ports_`, `listener_route_names_`) protected by `listener_mutex_`. Invariant: same length, same index = same listener.
- `signal_pipe_` for cross-thread wakeup (used by `stop()`).
- `run()` loop: 200 ms poll timeout, `rebuild_poll_set()`, `process_ready_fds()`, `process_all_sessions()`. **After loop exit, walks sessions and calls `shutdown_notify_client()` under `sessions_mutex_`** (commit `55e90d1a7`).
- `process_all_sessions()` only invokes `handler()` when there's actual work (revents set, buffered frames, or `to_process` flag) perf fix from commit `a2e99eed5`.
- Two timeouts: `HANDSHAKE_TIMEOUT_MS = 10s` for pre-auth, `IDLE_TIMEOUT_MS = 28800000` (8h) for established.
- `accept_new_connection()` enforces `max_sessions_`, sets TCP_NODELAY, attaches a closure that resolves identity via the `MysqlxConfigStore`.
- Connection cache LRU: scan from rbegin, match (hostgroup, user, schema, reusable); evict oldest at front when over `max_cached_`.
- `add_listener()` uses SO_REUSEADDR, accepts dotted-quad bind or "" INADDR_ANY.
- `remove_listener_for_route()` does **NOT** disturb in-flight sessions documented inline. Route-name lookup only happens at backend-target resolution (auth time), so an authenticated session continues regardless.
- `get_ssl_ctx()` returns `GloVars.get_SSL_ctx()` leverages the chassis-owned cert.
---
## Q. Mysqlx plugin: stats
### `plugins/mysqlx/include/mysqlx_stats.h` — NEW, 59 lines
### `plugins/mysqlx/src/mysqlx_stats.cpp` — NEW, 113 lines
- Per-route atomic counters with periodic SQLite flush.
- `MysqlxRouteStats` uses `std::atomic<uint64_t>` for conn_ok/err/used and bytes counters.
- `record_conn_*` take `mutex_` (not just atomic) so the `last_conn_err_` test sentinel is consistent with the increment.
- `flush_to_sqlite()` builds INSERT via `std::string` (not snprintf into 1024 buf fix for the silent-truncate bug on long route names) and `sqlite_escape()` doubles single quotes.
- Singleton `mysqlx_stats()`.
---
## R. Mysqlx plugin: build system
### `plugins/mysqlx/Makefile` — NEW, 150 lines
- Builds `ProxySQL_MySQLX_Plugin.so` standalone, sourcing repo-root includes.
- Auto-detects repo root by walking up for `src/proxysql_global.cpp` (12-hop limit + clear error).
- **Protobuf 3.x ABI guard:** `pkg-config --modversion protobuf`, fails fast unless major == 3 (the .pb.cc was generated with 3.21.12; protobuf 4.x changed SONAME). **Guard skipped on `clean`/`cleanall`** (commit `9f5ed235b`).
- `-fvisibility=hidden -fvisibility-inlines-hidden`: only `proxysql_plugin_descriptor_v1` is exported (it's `extern "C"`); avoids ODR collisions with proxysql core under `dlopen`.
- Hardening: `_FORTIFY_SOURCE=2` (skipped under ASan or `-O0`), `-fstack-protector-strong`, `-fPIC`.
- Propagates feature flags `PROXYSQL40/31/FFTO/TSDB/GENAI` from the top-level build.
- Static-links `deps/zstd/zstd/lib/libzstd.a` and `deps/lz4/lz4/lib/liblz4.a` since RTLD_LOCAL means transitive symbols from the proxysql binary are not visible.
- Dynamic links: `-lprotobuf -lssl -lcrypto`.
---
## S. Mysqlx plugin: generated protobuf
### `plugins/mysqlx/proto/` — 8 `.pb.cc` + 8 `.pb.h` (~23k LOC total)
- **Files:** `mysqlx`, `mysqlx_connection`, `mysqlx_session`, `mysqlx_datatypes`, `mysqlx_notice`, `mysqlx_sql`, `mysqlx_resultset`, `mysqlx_expect`.
- **Status:** Mechanically generated. **Do not review line-by-line.**
- **Source:** `test/deps/mysql-connector-c-8.4.0/.../plugin/x/protocol/protobuf/*.proto` via protoc 3.21.12.
- **Regen recipe** (from Makefile lines 22-27):
```
PROTO_SRC=test/deps/mysql-connector-c-8.4.0/mysql-8.4.0/plugin/x/protocol/protobuf
protoc --proto_path=$PROTO_SRC --cpp_out=plugins/mysqlx/proto \
mysqlx.proto mysqlx_connection.proto mysqlx_session.proto \
mysqlx_datatypes.proto mysqlx_notice.proto mysqlx_sql.proto \
mysqlx_resultset.proto mysqlx_expect.proto
```
- Compile flags: built with `-w` (warnings off) generator does not promise warning-free output across compilers.
---
## T. Tests, CI, infra
### T.1 New unit tests under `test/tap/tests/unit/`
All 28 are **NEW** and **gated under `PROXYSQL40=1`**. All are pure in-process no Docker, no MySQL backend; they link against `libproxysql.a` plus selected `plugins/mysqlx/src/*.cpp` source files compiled directly into the test binary with `-DMYSQLX_TEST_BUILD`. Several use a socketpair for fake I/O.
**Total: 985 `plan(...)` assertions** across the 28 unit tests (640 mysqlx-side + 345 plugin-chassis-side).
| File | LOC | `plan()` | Scope |
|---|---|---|---|
| `plugin_config_unit-t.cpp` | 364 | 48 | manager config-load + per-plugin variable round-trip |
| `plugin_dispatch_unit-t.cpp` | 389 | 52 | global `proxysql_dispatch_configured_plugin_admin_command` |
| `plugin_lifecycle_unit-t.cpp` | 294 | 26 | four-phase plugin lifecycle (Phase B then Phase D, ordering, partial rollback) |
| `plugin_manager_unit-t.cpp` | 464 | 96 | end-to-end manager driver (load/init/start/stop/unload, multi-plugin) |
| `plugin_prometheus_unit-t.cpp` | 110 | 10 | `get_prometheus_registry` returns `GloVars.prometheus_registry` |
| `plugin_query_hook_unit-t.cpp` | 283 | 46 | pre-execution query hook (`deny|allow`); manager-level register |
| `plugin_registry_unit-t.cpp` | 334 | 68 | in-process descriptor registry; admin-command alias dispatch |
| `mysqlx_admin_commands_unit-t.cpp` | 181 | 27 | `LOAD/SAVE MYSQLX TO/FROM RUNTIME\|MEMORY\|DISK` admin commands |
| `mysqlx_admin_disk_commands_unit-t.cpp` | 205 | 32 | `LOAD/SAVE MYSQLX TO/FROM DISK` against on-disk SQLite |
| `mysqlx_admin_schema_unit-t.cpp` | 141 | 25 | `mysqlx_users` and route table DDL string contents |
| `mysqlx_backend_auth_unit-t.cpp` | 302 | 42 | backend auth state machine; uses `inject_identity_for_test` (gated to `MYSQLX_TEST_BUILD`) |
| `mysqlx_compression_unit-t.cpp` | 991 | 64 | X-Protocol compression Phases 13 (zstd_stream + lz4_message) |
| `mysqlx_concurrent_unit-t.cpp` | 143 | 6 | N-session listener-on-real-port stress |
| `mysqlx_config_store_concurrent_unit-t.cpp` | 455 | 15 | multi-thread reader/writer race on `MysqlxConfigStore` |
| `mysqlx_config_store_pure_unit-t.cpp` | 365 | 25 | pure-API CRUD on `MysqlxConfigStore` |
| `mysqlx_config_store_unit-t.cpp` | 136 | 16 | smoke version of the above |
| `mysqlx_connection_unit-t.cpp` | 41 | 10 | `MysqlxConnection` state machine transitions |
| `mysqlx_credential_verify_unit-t.cpp` | 144 | 24 | credential-bytes verification (MYSQL41 / SHA256_MEMORY) |
| `mysqlx_data_stream_unit-t.cpp` | 89 | 18 | X-Protocol frame header parser (length+type, partial frames) |
| `mysqlx_message_dispatch_unit-t.cpp` | 661 | 49 | end-to-end message-type handler dispatch via socketpair-backed session |
| `mysqlx_protocol_socket_unit-t.cpp` | 373 | 20 | socket roundtrip (`read_one_message`, `send_message`) |
| `mysqlx_protocol_unit-t.cpp` | 315 | 42 | frame encode/decode + auth helper unit-isolation |
| `mysqlx_robustness_unit-t.cpp` | 1,509 | 74 | error/edge robustness: malformed frames, partial reads, listener reconcile, identity injection |
| `mysqlx_route_store_unit-t.cpp` | 440 | 26 | route selection strategies |
| `mysqlx_session_unit-t.cpp` | 637 | 62 | `MysqlxSession` lifecycle, `resolve_backend_target`, identity round-trip |
| `mysqlx_stats_unit-t.cpp` | 330 | 22 | `MysqlxStatsStore` and `stats_mysqlx_routes` flush |
| `mysqlx_thread_unit-t.cpp` | 147 | 22 | `MysqlxThread` listener/event-loop unit |
| `mysqlx_tls_unit-t.cpp` | 257 | 18 | TLS state on the data stream |
### T.2 New integration / e2e tests under `test/tap/tests/`
| File | LOC | `plan()` | Notes |
|---|---|---|---|
| `test_mysqlx_plugin_load-t.cpp` | 72 | 6 | dlopens the real `.so` and verifies registered tables. Pure in-process. |
| `test_mysqlx_admin_tables-t.cpp` | 364 | 43 | walks the registered table set after `init_all` and asserts DDL substrings. Pure in-process. |
| `test_mysqlx_e2e_handshake-t.cpp` | 427 | 10 (or skip_all) | MYSQL41 auth against real MySQL X port (33060). Requires running MySQL 8.x; reads `MYSQLX_E2E_HOST/PORT/USER/PASS`. |
| `test_mysqlx_e2e_routing-t.cpp` | 388 | 10 (or skip_all) | Same setup but pointed at ProxySQL (`MYSQLX_E2E_PROXYSQL_PORT`, default 16603). Requires both ProxySQL with mysqlx plugin AND a backend MySQL. |
E2e tests skip with `plan(skip_all => ...)` when target is unreachable, exit 0 this is the correct behaviour, but means CI can pass spuriously if the target was never wired. CI-mysqlx.yml fails fast if `test_mysqlx_*-t` glob is empty.
### T.3 Test build glue
#### `test/tap/tests/unit/Makefile` — MODIFIED, +321 / 4 lines
- Autodetects `PROXYSQL40` from `libproxysql.a` symbols (lines 199203). Same trick for FFTO / TSDB / GenAI / DEBUG.
- Adds `-DMYSQLX_TEST_BUILD` to `OPT` (line 243) so test-only methods on plugin classes compile in.
- New `FAKE_PLUGIN_SO` and `FAKE_PLUGIN2_SO` targets (lines 282299).
- New `mysqlx_plugin_build` target shells into `plugins/mysqlx` to produce the .so.
- 28 explicit per-test rules listing the exact plugin sources to drag in.
- `UNIT_TESTS` augmented inside `ifeq ($(PROXYSQL40),1) ... endif` block (lines 372404) none of these binaries exist on v3.0/v3.1 builds.
#### `test/tap/tests/Makefile` — MODIFIED, +47 / 7 lines
- Adds `PROXYSQL40_DETECTED` autodetect; under `!PROXYSQL40` filters `test_mysqlx_*` out of the wildcard glob.
- Symlinks `test_mysqlx_plugin_load-t` and `test_mysqlx_admin_tables-t` from `unit/`.
- Adds protobuf object compilation + explicit rules for e2e tests.
- Comment notes `test_mysqlx_listener_smoke-t` is retired (commit `df7e335e2`); coverage moved to `mysqlx_thread_unit-t` and `mysqlx_robustness_unit-t`.
### T.4 TAP groups manifest
#### `test/tap/groups/groups.json` — MODIFIED, +37 / 7 lines
- 32 new entries all carry `@proxysql_min_version:4.0`:
- 7 `plugin_*_unit-t` `unit-tests-g1`
- 21 `mysqlx_*_unit-t` `unit-tests-g1`
- 2 `test_mysqlx_admin_tables-t`, `test_mysqlx_plugin_load-t` `unit-tests-g1`
- 2 `test_mysqlx_e2e_{handshake,routing}-t` `mysqlx-e2e-g1`
Verified: `python3 test/tap/groups/check_groups.py` returns OK; `python3 test/tap/groups/lint_groups_json.py` returns OK.
#### `test/tap/groups/mysqlx-e2e/env.sh` — NEW, 19 lines
Sets `SKIP_PROXYSQL=1` (so `ensure-infras.bash` short-circuits before docker-compose), `MYSQLX_E2E_HOST/PORT/USER/PASS`, plus `MYSQLX_E2E_PROXYSQL_PORT=16603`. Header explicitly notes there is no `infras.lst` and no `infra-mysqlx/` those would be misleading. The dbdeployer-based local scripts in this dir are *not* in the CI critical path.
#### `test/tap/groups/mysqlx-e2e/setup-infras.bash` — NEW, 86 lines
Local-developer-only. Pulls dbdeployer, fetches MySQL 8.4 newest minimal, deploys single sandbox, creates `mysqlx_test` user, polls 33060.
#### `test/tap/groups/mysqlx-e2e/pre-cleanup.bash` — NEW, 18 lines
Stops and deletes the dbdeployer 8.4 sandbox.
### T.5 CI workflow
#### `.github/workflows/CI-mysqlx.yml` — NEW, 178 lines
Sole CI change in this PR. Triggers: `workflow_run` from `CI-trigger`, plus `workflow_dispatch`.
- **Job `unit-tests`** (ubuntu-22.04): sparse checkout, restore build cache, build the mysqlx plugin with all five tier flags (`PROXYSQL40=1 PROXYSQL31=1 PROXYSQLFFTO=1 PROXYSQLTSDB=1 PROXYSQLGENAI=1`), run `mysqlx_*_unit-t` and `plugin_*_unit-t`. `SKIP_PROXYSQL=1`. Loop fails fast on first non-zero exit.
- **Job `e2e-tests`** (depends on unit-tests): same checkout/cache/plugin-build dance, then dbdeployer setup of MySQL 8.4, create `mysqlx_test` user, poll 33060, source `env.sh`, run every `test_mysqlx_*-t` binary. `if: always()` cleanup of the sandbox.
### T.6 Test infrastructure
**No `test/infra/` changes on this HEAD.** The orphaned `test/infra/docker-compose-mysqlx.yml` and `test/tap/groups/mysqlx-e2e/infras.lst` were removed in commit `df7e335e2` and are absent. The mysqlx-e2e group does not use the docker-compose flow env.sh sets `SKIP_PROXYSQL=1` and CI uses dbdeployer inline.
---
## Inconsistencies / loose ends
The following were identified during review but are **not** correctness bugs they're places to verify or low-priority follow-ups:
1. **`mysqlx-e2e/setup-infras.bash` port assumption**: deploys the sandbox with `--port=13306` (classic protocol port) but `env.sh` advertises `MYSQLX_E2E_PORT=33060`. dbdeployer's default X protocol port for `single` is `classic_port + 20000`, so a 13306 deploy would expose X on **33306**, not 33060. CI-mysqlx.yml does NOT pass `--port=13306` so it gets the default (3306 / 33060). Local `setup-infras.bash` and CI therefore reach different ports. NOTE-level only the script is "ad-hoc local use, not in the CI critical path".
2. **e2e tests live in `tests/` but registered in `unit/` UNIT_TESTS list**: `test_mysqlx_plugin_load-t` and `test_mysqlx_admin_tables-t` live under `tests/` but are listed in `unit/Makefile`'s `UNIT_TESTS` and re-exposed via `ln -fs` from `tests/Makefile`. Intentional (they share libproxysql.a + plugin .so glue) but unusual.
3. **`groups.json` cosmetic churn**: unrelated reordering of group keys on six `test_mysql_*-t` lines (legacy-g3 row ordering). No semantic effect.
4. **Listener-removal does not disturb in-flight sessions**: documented in `mysqlx_thread.cpp:remove_listener_for_route()` as expected behaviour. If a future use case requires active disconnection, the path would walk `sessions_` on `identity_->default_route` match and call `shutdown_notify_client()`.
5. **TLS_PASSTHROUGH was removed but `mysqlx_variables.tls_mode` config column still accepts arbitrary strings**: future work should validate the column at runtime to reject unknown values, OR plumb through a real PASSTHROUGH implementation.
No registered-but-missing-source or built-but-unregistered tests. Lint passes.