# ProxySQL Plugin API ProxySQL supports dynamically loaded plugins via `.so` shared libraries. A plugin can extend ProxySQL with new protocols, admin tables, admin commands, or any other functionality by registering itself through a well-defined C++ ABI. ## Overview - Plugins are loaded at startup from paths specified in `proxysql.cnf`. - Each plugin is a shared library (`.so`) that exports a single C entry point. - ProxySQL calls the plugin's lifecycle hooks (`init`, `start`, `stop`) in order. - During `init`, the plugin receives a services struct with callbacks it can use to register tables, commands, access databases, and log messages. - ProxySQL does not know or care what a plugin does — the plugin self-registers everything it needs. ## Loading a Plugin Add the plugin path to `proxysql.cnf`: ``` plugins = ( "/path/to/my_plugin.so" ) ``` Multiple plugins can be listed: ``` plugins = ( "/path/to/protocol_a.so", "/path/to/protocol_b.so" ) ``` The `plugins` directive is read from the configuration **file** only. It is not persisted to the ProxySQL admin database. If the database exists when ProxySQL starts, other settings are loaded from the database instead of the config file, but the `plugins` list is always read from the config file (parsed in an early startup phase before the database takes precedence). ### Startup Sequence ProxySQL uses a **four-phase** plugin lifecycle. Every phase but Phase B is mandatory; Phase B is optional via the `register_schemas` descriptor field and only enabled when the plugin declares ABI version 2 or higher. 1. **Phase A — load.** ProxySQL parses `proxysql.cnf` and populates the `plugins` list. For each plugin path, ProxySQL calls `dlopen()`, resolves the `proxysql_plugin_descriptor_v1` symbol, and validates the descriptor (`abi_version`, `name`, callback pointers). 2. **Phase B — register_schemas (optional, ABI 2+).** If the descriptor wires `register_schemas`, the loader invokes it with a `ProxySQL_PluginServices` whose `register_table` / `register_command` / `register_command_alias` / `register_runtime_view` (ABI 3+) entries are LIVE but whose DB-handle getters (`get_admindb`, `get_configdb`, `get_statsdb`) are non-null stubs that return `nullptr`. The plugin declares the tables it owns, its admin commands, and any admin-side runtime views it wants the chassis to project from module state; it MUST NOT touch DB handles here. Plugins that leave `register_schemas` null (or that declare ABI 1) skip this phase entirely and do all their setup in Phase D. 3. **Phase C — admin materialization.** The admin module initializes and materializes the SQLite schemas collected during Phase B (`merge_plugin_tables` + `CREATE TABLE`). On DDL failure ProxySQL aborts startup. 4. **Phase D — init.** The plugin's `init()` callback is called, receiving a fully live `ProxySQL_PluginServices` (DB handles now valid). Plugins that opted out of Phase B register their tables AND commands here; plugins that used Phase B only finish their context setup. 5. **Phase E — start.** The plugin's `start()` callback is called. The plugin should start its threads, open listener sockets, and load runtime configuration. After this returns, ProxySQL is ready and the plugin is live. ### Shutdown Sequence 1. The plugin's `stop()` callback is called. 2. The plugin should stop its threads, close sockets, and release resources. 3. ProxySQL unloads the `.so`. ## The Plugin Contract A plugin must: 1. Be compiled as a shared library (`.so`) with the same C++17 toolchain as the ProxySQL core. 2. Export a single `extern "C"` function named `proxysql_plugin_descriptor_v1`. 3. Return a pointer to a static `ProxySQL_PluginDescriptor` struct. ### ABI Header All types are defined in `include/ProxySQL_Plugin.h`: ```cpp #include "ProxySQL_Plugin.h" ``` ### The Descriptor ```cpp struct ProxySQL_PluginDescriptor { const char *name; // Human-readable plugin name uint32_t abi_version; // PROXYSQL_PLUGIN_ABI_VERSION (1, 2, or 3) proxysql_plugin_init_cb init; // bool (*)(ProxySQL_PluginServices *) proxysql_plugin_start_cb start; // bool (*)() proxysql_plugin_stop_cb stop; // bool (*)() proxysql_plugin_status_json_cb status_json; // const char *(*)() proxysql_plugin_register_schemas_cb register_schemas; // ABI 2+, optional }; ``` | Field | Type | Description | |--------------------|---------------|-----------------------------------------------------------| | `name` | `const char*` | Plugin identifier, used in logging. | | `abi_version` | `uint32_t` | Set from `PROXYSQL_PLUGIN_ABI_VERSION`. Value `1` = pre-chassis descriptor (six fields). Value `2` = adds `register_schemas` (four-phase lifecycle). Value `3` = same descriptor layout as `2`; `ProxySQL_PluginServices` adds a tail-appended `register_runtime_view`. A v3/v3.1 ProxySQL core rejects `abi_version > 1`; the current PROXYSQL40 core accepts `[1, 3]`. | | `init` | callback | Phase D — called with live services; register tables and commands here (or finish context setup if `register_schemas` already did it). | | `start` | callback | Phase E — start threads, open sockets, load config. | | `stop` | callback | Called on shutdown. Pairs with `init`, not `start`: if `init` returned true and `start` later failed, `stop` is still called so the plugin can release resources it allocated in `init`. | | `status_json` | callback | Return a static JSON string describing plugin status. | | `register_schemas` | callback | Phase B (ABI 2+). Optional; leave null to skip Phase B entirely. Services passed here have `register_table` / `register_command` / `register_command_alias` / `register_runtime_view` LIVE but DB-handle getters returning `nullptr`. | All callbacks return `bool` (except `status_json` which returns `const char*`). Return `true` on success, `false` on failure. A `false` return from `register_schemas`, `init`, or `start` causes ProxySQL to exit. #### ABI version `include/ProxySQL_Plugin.h` exposes `PROXYSQL_PLUGIN_ABI_VERSION` (3 under PROXYSQL40, undefined in pre-chassis builds — the descriptor is then a legacy six-field struct with `abi_version = 1`). Plugins MUST assign `abi_version` from this macro rather than hard-coding a literal; the core's loader uses it to detect layout skew and reject plugins built for an unsupported ABI. ABI 3 keeps the descriptor layout identical to ABI 2 — the only addition is a tail-appended `register_runtime_view` field on `ProxySQL_PluginServices` — so plugins that compile against ABI 2 still load on the current core; the trailing services field is simply invisible to them. See `ProxySQL_Plugin.h` for the exact rules. ### The Entry Point ```cpp extern "C" const ProxySQL_PluginDescriptor *proxysql_plugin_descriptor_v1() { return &my_descriptor; } ``` ## Services Available to Plugins During `init()`, the plugin receives a `ProxySQL_PluginServices` struct containing function pointers the plugin can call: ```cpp struct ProxySQL_PluginServices { proxysql_plugin_register_table_cb register_table; proxysql_plugin_register_command_cb register_command; proxysql_plugin_snapshot_cb get_mysql_users_snapshot; proxysql_plugin_snapshot_cb get_mysql_servers_snapshot; proxysql_plugin_snapshot_cb get_mysql_group_replication_hostgroups_snapshot; proxysql_plugin_log_message_cb log_message; proxysql_plugin_db_handle_cb get_admindb; proxysql_plugin_db_handle_cb get_configdb; proxysql_plugin_db_handle_cb get_statsdb; // ABI 2 (PROXYSQL40) tail extensions: proxysql_plugin_register_query_hook_cb register_query_hook; proxysql_plugin_get_prometheus_registry_cb get_prometheus_registry; proxysql_plugin_register_command_alias_cb register_command_alias; // ABI 3 tail extension: proxysql_plugin_register_runtime_view_cb register_runtime_view; }; ``` ### Service Callbacks #### `register_table` ```cpp void register_table(const ProxySQL_PluginTableDef &def); ``` Register a SQLite table in one of ProxySQL's databases. Tables are created automatically before `start()` is called. ```cpp struct ProxySQL_PluginTableDef { ProxySQL_PluginDBKind db_kind; // Which database: admin_db, config_db, or stats_db const char *table_name; // Table name (e.g., "my_plugin_config") const char *table_def; // CREATE TABLE statement }; ``` `ProxySQL_PluginDBKind` values: | Value | Database | Purpose | |-------------|------------|------------------------------------------------------| | `admin_db` | In-memory | Runtime/admin tables, queryable via admin interface | | `config_db` | On-disk | Persistent configuration (survives restarts) | | `stats_db` | In-memory | Statistics/metrics tables | **Convention**: For configuration tables that support the standard memory↔runtime↔disk tier model, register the editable table in **both** `admin_db` and `config_db`. Register a separate `runtime_`-prefixed table in `admin_db` only — but treat it as an admin-side **projection**, not as a tier the plugin maintains: declare it via `register_table`, then declare a refresh callback for it via `register_runtime_view` (ABI 3+). The callback is invoked by the chassis before any admin SELECT against the table. This mirrors the canonical core pattern (`mysql_users` + `runtime_mysql_users`, where `runtime_mysql_users` is repopulated from the in-memory `MySQL_Authentication` state on demand). #### `register_command` ```cpp void register_command(const char *sql, proxysql_plugin_admin_command_cb cb); ``` Register an admin command handler. When a user issues the given SQL command through the admin interface, ProxySQL calls the registered callback. ```cpp ProxySQL_PluginCommandResult my_command( const ProxySQL_PluginCommandContext &ctx, const char *sql ); ``` **Important**: Command matching is case-insensitive with whitespace normalization. Only register the canonical form (e.g., `"LOAD MYPLUGIN USERS TO RUNTIME"`). Alias resolution (e.g., `"TO RUN"` → `"TO RUNTIME"`) must be handled in `Admin_Handler.cpp` in the ProxySQL core — plugins only see the canonical form. #### `log_message` ```cpp void log_message(int level, const char *message); ``` Log a message through ProxySQL's logging system. The numeric levels match ProxySQL's internal `proxy_*` severity scheme — anything other than 3 or 4 is emitted as info: | Level | Meaning | |-------|---------| | 3 | Error | | 4 | Warning | | any other value | Info | #### `get_admindb`, `get_configdb`, `get_statsdb` ```cpp SQLite3DB *get_admindb(); SQLite3DB *get_configdb(); SQLite3DB *get_statsdb(); ``` Return a pointer to the respective SQLite database. Use these to query and modify plugin tables at runtime. These are valid only during `start()` and later (not during `init()`). #### `get_mysql_users_snapshot`, `get_mysql_servers_snapshot`, `get_mysql_group_replication_hostgroups_snapshot` ```cpp SQLite3_result *get_mysql_users_snapshot(); SQLite3_result *get_mysql_servers_snapshot(); SQLite3_result *get_mysql_group_replication_hostgroups_snapshot(); ``` Return a snapshot of ProxySQL's internal MySQL topology state. These allow a plugin to access the current user list, server list, or group replication hostgroups without directly coupling to internal data structures. #### `register_runtime_view` (ABI 3+) ```cpp struct ProxySQL_PluginRuntimeView { const char *table_name; void (*refresh)(SQLite3DB *admindb, void *opaque); void *opaque; }; bool register_runtime_view(const ProxySQL_PluginRuntimeView &view); ``` Declare an admin-side **view** of plugin-module state. The named `table_name` lives in `admin_db` (typically `runtime_`) and holds no persistent rows — the chassis invokes `refresh(admindb, opaque)` before any admin `SELECT` that references the table as a whole identifier. The match is case-insensitive but identifier-aware: a query against `runtime__extra` (longer suffix) or `stats_runtime_
` (longer prefix) does NOT trigger the refresh for `runtime_
`. The refresh callback is expected to do (typically) `BEGIN; DELETE FROM
; INSERT/REPLACE INTO
...; COMMIT;` from the module's own in-memory state. The chassis deep-copies `table_name` so the plugin need not keep the pointed-to string alive after registration. The callback pointer must have static lifetime (typically a free function in the plugin `.so`). `opaque` is plugin-owned and passed back unchanged on each invocation; plugins that don't need it should pass `nullptr`. Returns `true` on successful registration, `false` if `table_name` is already registered (by this or another plugin) or if `refresh` is `nullptr`. `register_runtime_view` is live both during `register_schemas` (Phase B) and `init` (Phase D). Plugins typically register views alongside the editable tables they project. See the separation-of-duties contract under [Admin Integration Patterns](#admin-integration-patterns) below for why this exists. ## Admin Command Context and Result ### Context ```cpp struct ProxySQL_PluginCommandContext { SQLite3DB *admindb; SQLite3DB *configdb; SQLite3DB *statsdb; }; ``` Passed to every command callback. Provides direct access to the three databases. ### Result ```cpp struct ProxySQL_PluginCommandResult { int error_code; // 0 = success, non-zero = error uint64_t rows_affected; std::string message; // Optional message (empty = no message) }; ``` Return a result from your command callback: - `error_code == 0`: ProxySQL sends an OK packet to the client. - `error_code != 0`: ProxySQL sends an error packet with the message. ## Minimal Plugin Example This is a complete, minimal plugin that registers one table and one command: ```cpp // my_plugin.cpp #include "ProxySQL_Plugin.h" #include namespace { ProxySQL_PluginServices* g_services = nullptr; ProxySQL_PluginCommandResult handle_ping(const ProxySQL_PluginCommandContext&, const char*) { return {0, 0, "pong"}; } bool my_init(ProxySQL_PluginServices *services) { g_services = services; // Register a configuration table ProxySQL_PluginTableDef table { ProxySQL_PluginDBKind::admin_db, "my_plugin_config", "CREATE TABLE my_plugin_config (" " key VARCHAR NOT NULL PRIMARY KEY," " value VARCHAR NOT NULL DEFAULT ''" ")" }; services->register_table(table); // Register an admin command services->register_command("MYPLUGIN PING", &handle_ping); return true; } bool my_start() { // Start threads, open sockets, etc. return true; } bool my_stop() { // Stop threads, close sockets, etc. return true; } const char* my_status_json() { return "{\"name\":\"my_plugin\",\"state\":\"running\"}"; } const ProxySQL_PluginDescriptor my_descriptor = { "my_plugin", // name 1, // abi_version &my_init, // init &my_start, // start &my_stop, // stop &my_status_json // status_json }; } // namespace extern "C" const ProxySQL_PluginDescriptor *proxysql_plugin_descriptor_v1() { return &my_descriptor; } ``` ## Build Requirements ### Compiler Compatibility Plugins **must** be compiled with the same C++ compiler and standard library as the ProxySQL core. This is because `ProxySQL_PluginCommandResult` contains `std::string`, which has an ABI that varies between compilers and standard library versions. The safest approach is to build plugins within the ProxySQL build tree. ### Example Makefile ```makefile CXX = g++ CXXFLAGS = -std=c++17 -shared -fPIC -O2 INCLUDES = -I$(PROXYSQL_SRC)/include my_plugin.so: my_plugin.cpp $(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ $< clean: rm -f my_plugin.so ``` Build: ```bash make PROXYSQL_SRC=/path/to/proxysql ``` ### Linking Plugins are loaded with `RTLD_NOW | RTLD_LOCAL`. They should not link against `libproxysql.a` or any ProxySQL internal libraries. The plugin communicates with ProxySQL exclusively through the callbacks in `ProxySQL_PluginServices`. If a plugin needs SQLite3DB functionality (querying tables), it accesses it through the `get_admindb()` / `get_configdb()` / `get_statsdb()` service callbacks, not by linking against ProxySQL's SQLite wrapper. ## Admin Integration Patterns ### Separation of duties: Admin, the module, and the runtime view ProxySQL's three-tier configuration model is, in storage terms: ```text DISK (config_db) ↔ MEMORY (admin_db editable tables) ↔ RUNTIME (in-module state) ``` The crucial point is that **only the first two are persistent SQLite tables**. "RUNTIME" is the plugin module's in-memory state — typically an object guarded by its own mutex (e.g. `MysqlxConfigStore`). The `runtime_` table you register in `admin_db` is **not** module storage; it is an admin-side **view** of module state, projected on demand. Therefore the canonical division of work is: - **Admin** owns the editable tables (`` in both `admin_db` and `config_db`). - **The plugin module** owns the runtime state (an in-memory object). - The `runtime_` table in `admin_db` is repopulated by the plugin's refresh callback registered via `services.register_runtime_view(...)`. Concretely: | Command | What it does | |---------|--------------| | `LOAD TO RUNTIME` | Plugin reads the editable `admin_db.` and hands rows to its module via a typed install API that swaps state under the module's lock. **Does not touch `runtime_`.** | | `SAVE [FROM RUNTIME] TO MEMORY` | Plugin dumps its in-memory state and `REPLACE INTO`s the editable `admin_db.`. **Does not read `runtime_`.** | | `LOAD FROM DISK` / `SAVE TO DISK` | Plain `BEGIN/DELETE/INSERT/COMMIT` between `config_db.` and `admin_db.`. No module involvement. | | `SELECT ... FROM runtime_` (admin port) | Chassis fires the registered refresh callback, which wipes `runtime_` and re-projects the module's current state. | This mirrors the core's own `MySQL_Authentication` / `runtime_mysql_users` pattern (see `lib/ProxySQL_Admin.cpp::save_mysql_users_runtime_to_database`). #### Disk-tier sync invariant The disk-tier copies (LOAD/SAVE FROM/TO DISK) are still subject to the **empty-source-must-still-clear-destination** rule. Run the `DELETE`+`INSERT` unconditionally inside a single transaction and check each `execute()` return; an empty source means "no rows", not "leave the destination alone". PR #5643 fixed an early implementation that had this wrong on the disk path. The runtime path does not need this discipline because the module-side install API is a typed swap, not a copy: replacing the in-memory state with an empty set is a single atomic operation. ### Registering Admin Commands Commands are registered with the canonical form. Aliases (e.g., `TO RUN` for `TO RUNTIME`, `FROM MEM` for `FROM MEMORY`) are registered by the plugin via `register_command_alias` (ABI 2+); the chassis resolves incoming admin SQL to the canonical form before invoking the plugin's callback. There is no longer a hardcoded alias ladder in `Admin_Handler.cpp`. ### Table and view Registration Patterns ```cpp // Editable configuration table: visible in both admin and config databases. void register_config_table(ProxySQL_PluginServices& services, const char* name, const char* def) { services.register_table({ProxySQL_PluginDBKind::admin_db, name, def}); services.register_table({ProxySQL_PluginDBKind::config_db, name, def}); } // Admin-side projection of module state. Declare the empty table in // admin_db, then wire a refresh callback that reprojects from the // module before any admin SELECT. void register_runtime_view_table(ProxySQL_PluginServices& services, const char* name, const char* def, void (*refresh)(SQLite3DB*, void*), void* opaque) { services.register_table({ProxySQL_PluginDBKind::admin_db, name, def}); services.register_runtime_view({name, refresh, opaque}); } // Stats table: stats database only. void register_stats_table(ProxySQL_PluginServices& services, const char* name, const char* def) { services.register_table({ProxySQL_PluginDBKind::stats_db, name, def}); } ``` ## Limitations - **No hot-loading**: Plugins can only be loaded at startup. There is no `LOAD PLUGIN` command to load a plugin at runtime. ProxySQL must be restarted to add or remove plugins. - **No dependency resolution**: Plugins are loaded in the order listed in `proxysql.cnf`. If one plugin depends on another, the dependency must be listed first. - **ABI version range**: The current core accepts `abi_version` values in `[1, 3]`. Newly built plugins should set `abi_version = PROXYSQL_PLUGIN_ABI_VERSION`. - **Compiler coupling**: Plugins must match the ProxySQL core's C++ compiler and standard library due to `std::string` in `ProxySQL_PluginCommandResult`. ## Reference Implementation The MySQL X Protocol plugin (`plugins/mysqlx/`) is the reference implementation of a full ProxySQL plugin. It demonstrates: - Multi-file plugin structure with separate headers/sources - Custom `Makefile` within the ProxySQL build tree - Admin table registration (config + runtime + stats tables) - Admin command handlers with the three-tier model - Plugin-owned threads with listener sockets - TLS integration via ProxySQL's global SSL context - Connection pooling for backend connections - A standalone test suite using a custom test harness Key files: - `plugins/mysqlx/src/mysqlx_plugin.cpp` — Plugin entry point and lifecycle - `plugins/mysqlx/src/mysqlx_admin_schema.cpp` — Table and command registration - `plugins/mysqlx/Makefile` — Build configuration