45 KiB
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 — 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/andinclude/(the chassis core). - ~12,000 lines of test code under
test/tap/tests/unit/andtest/tap/tests/.
Sections A–G cover the chassis core. Sections H–O cover the mysqlx plugin. Sections P–T 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 PROXYSQL40so v3.x TUs see nothing. - Key contents:
PROXYSQL_PLUGIN_ABI_VERSION(currently3) and_MAX(also 3). ABI 1 = original 6-field descriptor; ABI 2 appendsregister_schemasfor the four-phase lifecycle; ABI 3 keeps the descriptor layout unchanged from ABI 2 and adds a singleregister_runtime_viewcallback at the tail ofProxySQL_PluginServicesso 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 viaextern "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, threeget_*dbgetters,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_tablelive, 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}plusproxysql_plugin_register_runtime_view_cb. The refresh callback gets a borrowedSQLite3DB*and re-projects module state into the named admin-db table on demand. - Header explicitly calls out the
std::string/prometheus-cppC++-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_schemasis only dereferenced whenabi_version >= 2. - Verify
register_runtime_viewlives at the END ofProxySQL_PluginServices; older plugins compiled against the ABI-2 layout never see the field. - Verify the loader rejects
abi_version > PROXYSQL_PLUGIN_ABI_VERSION_MAXrather than reading past its struct.
- Verify
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; plustables(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-freehas_configured_plugin_query_hook),resolve_configured_plugin_admin_alias, andproxysql_refresh_configured_plugin_runtime_views(admin pre-SELECT hook). - Internal
plugin_handle_tcarries dlopen handle +schemas_registered/initialized/started/stoppedstate flags.runtime_views_vector holdsregistered_runtime_view_t{table_name, refresh, opaque}entries. - Two services structs:
services_(Phase D, full services) andservices_phase_b_(Phase B, with stubbed DB getters and stubbedregister_query_hook).register_runtime_viewis wired in BOTH structs — plugins typically declare runtime views alongside their tables inregister_schemas, so the callback is live in Phase B as well as Phase D.
- Spot-check:
- Lifecycle invariant:
proxysql_init_configured_pluginsmust run BEFORE any worker thread takes the lock-free read path. resolve_alias_to_canonicalreturnsstd::stringby value (not borrowedc_str()) so a concurrent reload between resolve and dispatch can't dangle a pointer.
- Lifecycle invariant:
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_manageris astd::atomic<ProxySQL_PluginManager*>; reads/writes coordinated viag_active_plugin_manager_mutex(std::shared_mutex— readers from dispatch/resolve paths share, publishers/unpublishers take unique). A separatestd::mutex g_plugin_lifecycle_mutexserializes 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 tog_registry_targetset by aScopedRegistryTargetRAII guard around plugin callbacks;note_registration_failurerecords the first failure. sql_references_table_ci()is the matcher used byrefresh_runtime_views_for_query: case-insensitive whole-identifier substring match treating[A-Za-z0-9_]as identifier characters. Soruntime_mysqlx_usersmatches inSELECT * FROM `runtime_mysqlx_users`but NOT inruntime_mysqlx_users_extraorstats_runtime_mysqlx_users.load()(lines 324–383): rejects duplicate paths;dlopen(RTLD_NOW|RTLD_LOCAL); resolvesproxysql_plugin_descriptor_v1; rejects null/emptynameandabi_versionoutside[1, PROXYSQL_PLUGIN_ABI_VERSION_MAX].invoke_register_schemas_phase()(lines 386–461): Phase B; only readsdescriptor->register_schemaswhenabi_version >= 2; snapshotstables_*/commands_/storage sizes and rolls back on failure to keep retries idempotent.init_all()has the same snapshot/rollback contract.stop_all()(lines 548–576): pairsstop()withinit(), NOT withstart()— every plugin whose init succeeded gets stop, even if its own start failed (teardown symmetry to avoid leaks). Plugins are markedstopped=trueeven on failure; idempotent across destructor.canonicalize_plugin_command(): trims, strips trailing;, collapses internal whitespace.- Alias collision rules in
register_command_alias(lines 765–809): rejects shadowing another command's canonical or alias; idempotent for duplicates. - Publish ordering in
proxysql_load_configured_plugins(lines 896–963): manager is installed as active before Phase D soProxySQL_Admin::initcan read tables viaproxysql_get_plugin_manager. Comments call out the unsafe-reordering hazard. stop_configured_pluginsclears the active pointer before callingstop_all, thenmanager.reset()always runs (so the.sois unmapped even if a plugin's stop returned false).
- Concurrency model:
- Spot-check:
- Confirm the
descriptor->register_schemasaccess is gated behind theabi_version >= 2ucheck. - Verify the
RTLD_LOCALflag — no plugin should pollute the global symbol table. - Confirm
proxysql_dispatch_configured_plugin_query_hookcallsg_active_plugin_manager.load()AFTER taking the shared lock; the lock-freeproxysql_has_configured_plugin_query_hookis documented to allow false positives.
- Confirm the
C. Chassis: admin integration
lib/Admin_Bootstrap.cpp — MODIFIED, +57 lines
- Purpose: merges plugin-declared schemas into
tables_defs_{admin,config,stats}so the existingcheck_and_build_standard_tablesDDL pass materializes them. - Key contents:
ProxySQL_Admin::init()consultsproxysql_get_plugin_manager(), walks eachProxySQL_PluginDBKind, dedup-checks against existing tables, and appends viainsert_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 thatsrc/main.cppactually checks the return value. - Note the indentation churn around
#ifdef PROXYSQLGENAI— review for accidental scope changes.
- Verify
lib/Admin_Handler.cpp — MODIFIED, +43 lines
- Purpose: generic dispatch path replacing the previous hard-coded MYSQLX alias ladder.
- Key contents:
admin_session_handler()callsproxysql_resolve_configured_plugin_admin_alias(query_str)early; if a canonical comes back, the handler invokesSPA->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_canonicalis held by value (std::string), not as a raw pointer. - Verify the new branch sits BEFORE the generic LOAD/SAVE handler.
- Confirm
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()returnGloAdmin->{admindb,configdb,statsdb}ifGloAdminis non-null. Gated byPROXYSQL40so v3.x exports no plugin-aware symbols at all. ProxySQL_Admin::dispatch_plugin_admin_command<S>(sess, sql)builds aProxySQL_PluginCommandContext{admindb,configdb,statsdb}, callsproxysql_dispatch_configured_plugin_admin_command, and translatesresultintosend_ok_msg_to_client/send_error_msg_to_client.- Explicit template instantiations for
MySQL_SessionandPgSQL_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 onif (admin)and placed outside the existingif (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 thesql_references_table_cimatcher) — a query that touches no registered view is a cheap no-op (one shared lock + N substring scans). Therefreshflag is left untouched; it gates a separate set of core-only refreshes (stats_mysql_processlist, runtime_mysql_users, etc.).
- Three free functions
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
GloPluginManagerunique_ptr and threads it throughProxySQL_Main_init_phase2. - Key contents:
static std::unique_ptr<ProxySQL_PluginManager> GloPluginManager;(file scope).- Four wrapper functions:
LoadConfiguredPlugins,InitConfiguredPlugins,StartConfiguredPlugins,StopConfiguredPlugins. Eachexit(EXIT_FAILURE)on error during startup; shutdown only logs. UnloadPlugins()now callsStopConfiguredPluginsfirst, 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_modulenow checksGloAdmin->init()return and exits on failure.- Config parsing:
proxysql_load_plugin_modules_from_config(root, GloVars.plugin_modules)from the libconfigSetting&.
- Spot-check:
- Phase-D-before-workers ordering: confirm
Init/StartConfiguredPluginscomplete before the worker-thread spawning later inProxySQL_Main_init_phase3___start_all. - Verify
GloPluginManageris at file scope, not function-local — destructor needs to run during process teardown.
- Phase-D-before-workers ordering: confirm
include/proxysql_glovars.hpp and lib/ProxySQL_GloVars.cpp — MODIFIED, +25 / +26 lines
- Purpose: add the
plugin_modulesstd::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 inproxysql_glovars.hppdirectly withoutproxysql_structs.h). - Free function
proxysql_load_plugin_modules_from_config(const Setting& root, std::vector<std::string>& plugin_modules): clears, readspluginssetting, pushes each string entry. Silently skips non-string entries. - Constructor and destructor explicitly clear
plugin_modules.
- Header gains a forward decl of
E. Chassis: build system
Makefile (top-level) — MODIFIED, +41 / −14 lines
- Purpose: introduces
PROXYSQL40tier, propagates the macro and runs the mysqlx plugin sub-build. - Key contents:
- Tier hierarchy:
PROXYSQLGENAI⇒PROXYSQL40⇒PROXYSQL31. PROXYSQL40=1triggers a major-version bump (3.0.x → 4.0.x) via the same awk recipe.- Each
build_src_*target conditionallycd plugins/mysqlx && $(MAKE)(skipped ifPROXYSQL40unset). clean*targets descend intoplugins/mysqlx. Top-levelcleanbuildrecurses unconditionally; the plugin Makefile gates its protobuf check to avoid breaking onclean(commit9f5ed235b).install/uninstalladd/usr/lib/proxysql/plugins/ProxySQL_MySQLX_Plugin.so.
- Tier hierarchy:
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 and 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_hookechoespayload.query_textback; 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
MysqlxPluginContextand the listener-reconcile entry points. MysqlxPluginContextholdsservicespointer (chassis ABI),unique_ptr<MysqlxConfigStore>,vector<unique_ptr<Mysqlx_Thread>>,route_to_threadmap + mutex +next_rr_indexfor 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_PluginDescriptorentry exported viaproxysql_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()— everyadmindb.execute()return is checked; defensive ROLLBACK on any failure including post-COMMIT, addresses the v3.0 silent-wipe bug.mysqlx_start()doessync_disk_to_memory()(configdb → editable mysqlx_* tables) then fourinstall_<X>_from_admincalls to populate the in-memoryMysqlxConfigStoredirectly from the editable admin tables — noruntime_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 drivesmysqlx_reconcile_listeners()on the same path used byLOAD MYSQLX ROUTES TO RUNTIME.parse_bind_addr()handles bothhost:portand[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 theLOAD/SAVEadmin commands. - Four config-table pairs (memory + runtime):
mysqlx_users,mysqlx_routes,mysqlx_backend_endpoints,mysqlx_variables, all withJSON_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 viaregister_command_alias. - LOAD/SAVE callbacks no longer copy between admin tables. Each
load_<X>_to_runtimecallback invokesMysqlxConfigStore::install_<X>_from_admin(admindb, err)(read editable mysqlx_, swap into the module's in-memory state under its own lock). Eachsave_<X>_from_runtimecallback invokesMysqlxConfigStore::save_<X>_to_admin_table(admindb)(dump module state into the editable mysqlx_ 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_vieware registered at schema-registration time viaservices.register_runtime_view({"runtime_mysqlx_<X>", &refresh_<X>_runtime_view, nullptr}). Each callsMysqlxConfigStore::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_runtimecallsmysqlx_reconcile_listenersweak-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(wasMysqlxCredentials— renamed in84bbdfdca); now carries acommentfield.MysqlxRoutealso carries acommentfield.MysqlxBackendAuthModeenum:mapped/service_account/pass_through.- Public struct
MysqlxBackendEndpointOverride{hostname, mysql_port, mysqlx_port, use_ssl, attributes, comment}— promoted from a file-localMysqlxEndpointOverrideso 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; convenienceinstall_all_from_admin(db, err)(test-only fixture helper);snapshot_active_routes()returningvector<pair<name, bind>>for the listener reconciler; plus the read-sideresolve_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 editablemysqlx_usersplus cross-moduleruntime_mysql_users(canonical password/hostgroup);install_endpoints_from_admin()reads the editablemysqlx_backend_endpointsand cross-moduleruntime_mysql_servers WHERE status='ONLINE'. None of the install paths touch anyruntime_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()supportsfirst_availableandround_robin[_with_fallback]; round-robin uses a separaterr_mutex_so RR state doesn't interfere with config swaps.- Test-only
install_for_testis unconditionally available — note it bypassesinstall_*_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 ismysqlx_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()tohost:portstring; 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 againstruntime_mysqlx_routes. An inline comment calls out why we deliberately do NOT readruntime_mysqlx_routeshere: 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 everyLOAD MYSQLX ROUTES TO RUNTIMEcall. - RR cursor
next_rr_indexis wrapped via((next_rr_index % pool) + pool) % pool— defensive against negative. - Note: if
add_listenerfails (port in use), the route is silently not added toroute_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, blockingread_exact/write_all,send_error/send_okover 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 usesCRYPTO_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_SIZEso the +1 cannot wrap.mysqlx_send_error/okuse 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 completeMysqlxFrames,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 commit4bd4b462b) scrubs only buffers and parse-error, preserving SSL/BIO/encrypted state — used byMysqlxConnection::reset()between pool reuses.try_parse_frame()validates1 <= payload_size <= 16 MB, setsparse_error_=trueon out-of-range, slides read window after >4 KiB consumed.- SSL path uses memory BIOs:
read_from_net()pumpsrecv → BIO_write(rbio_ssl_), thenSSL_readintofeed_bytes.write_to_net()SSL_writes app data, thenqueue_encrypted_output()viaBIO_read(wbio_ssl_)andflush_ssl_write_buf(). do_ssl_handshake()post-handshake drain: 64 KiBstatic thread_localscratch (commit55e90d1a7) — 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, optionalSSL_CTX*, and a 10-stateBackendAuthStateenum. start_connect()usesinet_pton(AF_INET, host, ...)and rejects non-IPv4-literal hostnames (commit55e90d1a7— previously the return was discarded and a hostname silently became 0.0.0.0).check_connect()polls with timeout vsconnect_timeout_ms_, checksSO_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():ParseFromArrayreturn is now checked (commit4bd4b462b); previously a malformed AuthenticateContinue produced an undefined-input scramble.reset()callsbackend_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). Statusenum driveshandler(). 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 behindMYSQLX_TEST_BUILD(commit04bccec51); read-onlytarget_*_for_testgetters remain unconditional. MysqlxTlsMode: onlyTLS_OFF/TLS_TERMINATEremain — the deadTLS_PASSTHROUGHwas removed in55e90d1a7(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); recordsmysqlx_stats().record_conn_err().shutdown_notify_client()(L1143) — commit55e90d1a7: enqueues 1053 fatal Error, single-passwrite_to_net(),SSL_set_quiet_shutdown(1)+SSL_shutdown, thenstatus=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 bylistener_mutex_. Invariant: same length, same index = same listener. signal_pipe_for cross-thread wakeup (used bystop()).run()loop: 200 ms poll timeout,rebuild_poll_set(),process_ready_fds(),process_all_sessions(). After loop exit, walks sessions and callsshutdown_notify_client()undersessions_mutex_(commit55e90d1a7).process_all_sessions()only invokeshandler()when there's actual work (revents set, buffered frames, orto_processflag) — perf fix from commita2e99eed5.- Two timeouts:
HANDSHAKE_TIMEOUT_MS = 10sfor pre-auth,IDLE_TIMEOUT_MS = 28800000(8h) for established. accept_new_connection()enforcesmax_sessions_, sets TCP_NODELAY, attaches a closure that resolves identity via theMysqlxConfigStore.- 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()returnsGloVars.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.
MysqlxRouteStatsusesstd::atomic<uint64_t>for conn_ok/err/used and bytes counters.record_conn_*takemutex_(not just atomic) so thelast_conn_err_test sentinel is consistent with the increment.flush_to_sqlite()builds INSERT viastd::string(not snprintf into 1024 buf — fix for the silent-truncate bug on long route names) andsqlite_escape()doubles single quotes.- Singleton
mysqlx_stats().
R. Mysqlx plugin: build system
plugins/mysqlx/Makefile — NEW, 150 lines
- Builds
ProxySQL_MySQLX_Plugin.sostandalone, 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 onclean/cleanall(commit9f5ed235b). -fvisibility=hidden -fvisibility-inlines-hidden: onlyproxysql_plugin_descriptor_v1is exported (it'sextern "C"); avoids ODR collisions with proxysql core underdlopen.- Hardening:
_FORTIFY_SOURCE=2(skipped under ASan or-O0),-fstack-protector-strong,-fPIC. - Propagates feature flags
PROXYSQL40/31/FFTO/TSDB/GENAIfrom the top-level build. - Static-links
deps/zstd/zstd/lib/libzstd.aanddeps/lz4/lz4/lib/liblz4.asince 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/*.protovia 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 |
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 1–3 (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
PROXYSQL40fromlibproxysql.asymbols (lines 199–203). Same trick for FFTO / TSDB / GenAI / DEBUG. - Adds
-DMYSQLX_TEST_BUILDtoOPT(line 243) so test-only methods on plugin classes compile in. - New
FAKE_PLUGIN_SOandFAKE_PLUGIN2_SOtargets (lines 282–299). - New
mysqlx_plugin_buildtarget shells intoplugins/mysqlxto produce the .so. - 28 explicit per-test rules listing the exact plugin sources to drag in.
UNIT_TESTSaugmented insideifeq ($(PROXYSQL40),1) ... endifblock (lines 372–404) — none of these binaries exist on v3.0/v3.1 builds.
test/tap/tests/Makefile — MODIFIED, +47 / −7 lines
- Adds
PROXYSQL40_DETECTEDautodetect; under!PROXYSQL40filterstest_mysqlx_*out of the wildcard glob. - Symlinks
test_mysqlx_plugin_load-tandtest_mysqlx_admin_tables-tfromunit/. - Adds protobuf object compilation + explicit rules for e2e tests.
- Comment notes
test_mysqlx_listener_smoke-tis retired (commitdf7e335e2); coverage moved tomysqlx_thread_unit-tandmysqlx_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
- 7
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), runmysqlx_*_unit-tandplugin_*_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, createmysqlx_testuser, poll 33060, sourceenv.sh, run everytest_mysqlx_*-tbinary.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:
-
mysqlx-e2e/setup-infras.bashport assumption: deploys the sandbox with--port=13306(classic protocol port) butenv.shadvertisesMYSQLX_E2E_PORT=33060. dbdeployer's default X protocol port forsingleisclassic_port + 20000, so a 13306 deploy would expose X on 33306, not 33060. CI-mysqlx.yml does NOT pass--port=13306so it gets the default (3306 / 33060). Localsetup-infras.bashand CI therefore reach different ports. NOTE-level only — the script is "ad-hoc local use, not in the CI critical path". -
e2e tests live in
tests/but registered inunit/UNIT_TESTS list:test_mysqlx_plugin_load-tandtest_mysqlx_admin_tables-tlive undertests/but are listed inunit/Makefile'sUNIT_TESTSand re-exposed vialn -fsfromtests/Makefile. Intentional (they share libproxysql.a + plugin .so glue) but unusual. -
groups.jsoncosmetic churn: unrelated reordering of group keys on sixtest_mysql_*-tlines (legacy-g3 row ordering). No semantic effect. -
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 walksessions_onidentity_->default_routematch and callshutdown_notify_client(). -
TLS_PASSTHROUGH was removed but
mysqlx_variables.tls_modeconfig 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.