#ifndef PROXYSQL_PLUGIN_H #define PROXYSQL_PLUGIN_H // Plugin chassis is a v4.0 feature. Including this header from a v3.x // (no PROXYSQL40) translation unit yields no declarations -- v3.0/v3.1 // have no plugin concept whatsoever. Plugins built for PROXYSQL40 set // abi_version to PROXYSQL_PLUGIN_ABI_VERSION; the loader rejects // abi_version values it doesn't understand. #ifdef PROXYSQL40 #include #include class SQLite3DB; class SQLite3_result; namespace prometheus { class Registry; } // Descriptor ABI version the plugin was compiled for. Plugins set // `abi_version` on their ProxySQL_PluginDescriptor literal to this macro // so the core loader can detect layout skew between plugin and loader // builds. // // ABI 1: original 6-field descriptor (name, abi_version, init, start, // stop, status_json). Pre-chassis build. // ABI 2: appends `register_schemas` (four-phase lifecycle, PROXYSQL40). // ABI 3: same descriptor layout as ABI 2; ProxySQL_PluginServices grows // a `register_runtime_view` field at the end so plugins can // declare admin-side projections of module state. Plugins that // stay on ABI 2 keep working — they simply don't see the new // field in their compiled-against struct, and core never // dereferences past the ABI-2 layout for them. #define PROXYSQL_PLUGIN_ABI_VERSION 3u #define PROXYSQL_PLUGIN_ABI_VERSION_MAX 3u enum class ProxySQL_PluginDBKind : uint8_t { admin_db = 0, config_db = 1, stats_db = 2 }; // The manager deep-copies table_name and table_def; the plugin need not keep // the pointed-to strings alive after register_table returns. struct ProxySQL_PluginTableDef { ProxySQL_PluginDBKind db_kind; const char *table_name; const char *table_def; }; // Borrowed DB handles valid for the duration of the admin command callback. // Must not be stored beyond the callback invocation. struct ProxySQL_PluginCommandContext { SQLite3DB *admindb; SQLite3DB *configdb; SQLite3DB *statsdb; }; // NOTE: ProxySQL_PluginCommandResult contains std::string. Plugins MUST // be compiled with the same C++ standard library (same compiler, same // -std= flag, same libstdc++/libc++ version) as the ProxySQL core. // This is guaranteed when plugins are built within the ProxySQL build // tree. Third-party plugins must match the core's build environment. struct ProxySQL_PluginCommandResult { int error_code; uint64_t rows_affected; std::string message; }; using proxysql_plugin_admin_command_cb = ProxySQL_PluginCommandResult (*)(const ProxySQL_PluginCommandContext &, const char *); using proxysql_plugin_register_table_cb = void (*)(const ProxySQL_PluginTableDef &); using proxysql_plugin_register_command_cb = void (*)(const char *, proxysql_plugin_admin_command_cb); #ifdef PROXYSQL40 // Register an alternative spelling (alias) of an already-registered command. // The `canonical` argument MUST match the exact SQL passed to a prior // `register_command()` call (after whitespace normalization). Plugins use // this to publish user-friendly spellings — "LOAD MYSQLX USERS FROM MEMORY" // vs "LOAD MYSQLX USERS TO RUNTIME" — without replicating the canonical // callback. Core's admin dispatcher consults the alias table to resolve // incoming SQL to the canonical form before invoking the registered // callback. // // No-op if `canonical` isn't registered yet (plugins should register the // command BEFORE its aliases). Silently skips duplicate aliases. using proxysql_plugin_register_command_alias_cb = void (*)(const char *canonical, const char *alias); #endif /* PROXYSQL40 */ using proxysql_plugin_snapshot_cb = SQLite3_result *(*)(); using proxysql_plugin_db_handle_cb = SQLite3DB *(*)(); using proxysql_plugin_log_message_cb = void (*)(int, const char *); #ifdef PROXYSQL40 // Pre-execution query hook (Step 2 ABI extension). // // STATUS (chassis ABI 2 — initial baseline): the registration and // dispatch path are wired through ProxySQL_PluginManager and the // chassis fast-path probe (proxysql_has_configured_plugin_query_hook), // and unit tests exercise the dispatch surface end-to-end. However, // the production data plane (MySQL_Session::handler___status_WAITING_ // CLIENT_DATA___STATE_SLEEP___MYSQL_COM_QUERY_qpo and the matching // PgSQL_Session COM_QUERY entry) does NOT yet call the dispatch // helper. Plugins that register a query hook today will see the // callback invoked from unit tests but not from real client traffic. // Wiring the production fast-path is a deliberate follow-up — search // for "TODO(plugin-query-hook)" in lib/MySQL_Session.cpp and // lib/PgSQL_Session.cpp for the precise injection points. // // Wire protocol the hook is being invoked for. A plugin can register // independently for each protocol; one hook per protocol per plugin. enum class ProxySQL_PluginProtocol : uint8_t { mysql = 0, pgsql = 1 }; // Payload handed to a query-hook callback. All pointers are owned by // core and remain valid only for the duration of the callback. The // callback must not retain them or mutate the underlying buffers. // // query_text is the SQL the client sent, NOT NUL-terminated; query_len // is its length in bytes. user / client_ip / schema are NUL-terminated // C strings and may be empty (never NULL). struct ProxySQL_PluginQueryHookPayload { const char *user; const char *client_ip; const char *schema; const char *query_text; uint32_t query_len; }; // Outcome of a query hook. ALLOW lets the query proceed to the // backend. DENY returns an error to the client and the query never // dispatches; the message is copied by core, the plugin need not keep // it alive after the callback returns. // // NOTE: same std::string ABI coupling caveat as // ProxySQL_PluginCommandResult applies. enum class ProxySQL_PluginQueryHookAction : uint8_t { allow = 0, deny = 1 }; struct ProxySQL_PluginQueryHookResult { ProxySQL_PluginQueryHookAction action; std::string message; }; using proxysql_plugin_query_hook_cb = ProxySQL_PluginQueryHookResult (*)(const ProxySQL_PluginQueryHookPayload &); // register_query_hook(proto, cb). Returns true on success, false if a // hook for that protocol is already registered. Valid only during the // init callback (same lifetime rule as register_table / register_command). using proxysql_plugin_register_query_hook_cb = bool (*)(ProxySQL_PluginProtocol, proxysql_plugin_query_hook_cb); // Returns the prometheus::Registry* that core uses for its own metrics. // Plugins register their counters / gauges / histograms against this // shared registry using prometheus-cpp directly; their metrics then // surface at the same /metrics endpoint scrapers already poll. // // NOTE: prometheus-cpp is a C++ library with C++ ABI surface. Same // build-environment caveat applies as to std::string in this header: // plugins must be compiled in the ProxySQL build tree (or at least // against a matching prometheus-cpp version + matching libstdc++). // // Lifetime: GloVars and its prometheus registry are constructed // before any plugin is loaded, so the returned pointer is non-null // for every callback (init, start, admin command callback, query // hook). Plugins may register metrics in init() if they want them // scraped immediately. using proxysql_plugin_get_prometheus_registry_cb = prometheus::Registry* (*)(); // Runtime-view projection (ABI 3 extension). // // Mirrors the canonical mysql_users / runtime_mysql_users pattern: Admin // owns the editable table (e.g. mysqlx_users), the plugin module owns // the runtime state (e.g. MysqlxConfigStore::identities_), and the // runtime_ in admin_db is an Admin-side *view* projected on // demand from the module. The view holds no persistent rows — it is // wiped and refilled by the plugin's refresh callback before any admin // SELECT touches it. // // Plugins call register_runtime_view(view) during register_schemas or // init. Each registration ties an admin-db table name (e.g. // "runtime_mysqlx_users") to a refresh callback. Admin's pre-SELECT // hook walks every registered view and invokes the callback for any // that the SQL references. // // The refresh callback gets a borrowed admindb handle and is expected // to do (typically) `BEGIN; DELETE FROM
; INSERT/REPLACE INTO //
...; COMMIT;` from the module's in-memory state. The chassis // passes admindb explicitly (rather than the plugin reaching for it via // services.get_admindb) because the same callback might be invoked // against snapshot DBs in the future without the global-getter detour. // // Lifetime: the chassis deep-copies `table_name`, so the plugin need // not keep the pointed-to string alive. The callback pointer itself // must point at a function with static lifetime (typically a free // function in the plugin .so). `opaque` is plugin-owned; it is 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. struct ProxySQL_PluginRuntimeView { const char *table_name; void (*refresh)(SQLite3DB *admindb, void *opaque); void *opaque; }; using proxysql_plugin_register_runtime_view_cb = bool (*)(const ProxySQL_PluginRuntimeView &); #endif /* PROXYSQL40 */ // Services provided to plugins across the four-phase lifecycle. // // Availability by phase: // * register_schemas (Phase B, optional, run between load() and init()): // - register_table: LIVE (writes to pending-tables list) // - register_command: LIVE (orthogonal to schema, OK here) // - log_message: LIVE // - get_prometheus_registry: LIVE (registry is constructed at startup) // - get_admindb/get_configdb/get_statsdb: RETURN nullptr (admin module // has not yet materialized the schema). Plugins using this callback // MUST NOT touch DB handles here; save that work for init(). // - register_query_hook: RETURNS false (not yet wired). // - snapshots: RETURN nullptr (see below). // * init (Phase D): register_*, log_message, get_*db and // get_prometheus_registry are LIVE. Snapshot getters remain stubs — // see the note below. // * start (Phase E) and beyond: get_*db, log_message, // get_prometheus_registry remain valid; register_* are no-ops (ignored // with a warning — schemas must be declared before start). // // NOTE ON SNAPSHOT GETTERS (`get_mysql_users_snapshot`, etc.): // These are currently wired to a stub that returns nullptr in every phase. // The plan is to surface read-only SQLite3_result snapshots of the core's // runtime config tables, but the backing plumbing (snapshot acquisition, // lifetime, invalidation on reload) isn't implemented yet. Plugins MUST // treat a nullptr return as "snapshot not available"; do not assume // non-null just because you're in Phase D. When the feature lands, only // the nullptr contract will change — the field signatures won't. 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; #ifdef PROXYSQL40 // Step 2 ABI extensions. Both fields are additive at the end of // the struct -- older plugins that were built against the previous // layout don't read past the previous member; new plugins must // check non-null before calling. proxysql_plugin_register_query_hook_cb register_query_hook; proxysql_plugin_get_prometheus_registry_cb get_prometheus_registry; // Step 2.5 extension: register a user-friendly alias for a command // already registered via register_command. Admin's dispatcher // resolves aliases to canonical before invoking the callback, so // plugins avoid the MYSQLX-specific hardcoded alias ladder that // previously lived in lib/Admin_Handler.cpp. proxysql_plugin_register_command_alias_cb register_command_alias; // ABI-3 extension: declare an admin-side view of module state // (canonical pattern, mirrors Admin's own runtime_mysql_users / // save_mysql_users_runtime_to_database flow). See the contract // next to ProxySQL_PluginRuntimeView above. May be nullptr in // services_phase_b_ — register_runtime_view is live both during // register_schemas() and init(); plugins typically register views // at the same point they register their tables, so the callback // is wired in both phases. proxysql_plugin_register_runtime_view_cb register_runtime_view; #endif /* PROXYSQL40 */ }; using proxysql_plugin_init_cb = bool (*)(ProxySQL_PluginServices *); using proxysql_plugin_start_cb = bool (*)(); // stop() pairs with init() for teardown, not with start(). The loader // guarantees that every plugin whose init() returned true will get stop() // called exactly once -- even if its own start() returned false, and even // if a later plugin's start() caused shutdown mid-startup. Plugins must // therefore be able to tear down resources they allocated in init (config // stores, caches, worker pools not yet spawned, ...) without having seen // a matching start(). A plugin whose init() returned false never sees // stop(). using proxysql_plugin_stop_cb = bool (*)(); // Returned pointer must have static storage duration (string literal or static // buffer). The caller does not free it. using proxysql_plugin_status_json_cb = const char *(*)(); #ifdef PROXYSQL40 // Phase B entry point: "declare your schema before admin bootstrap." // // Four-phase plugin lifecycle: // Phase A: load() -- dlopen the .so, read the descriptor // Phase B: register_schemas() -- NEW, optional; plugin returns its table defs // Phase C: admin module init -- core materializes SQLite schema from the // plugin-registered defs (merge_plugin_tables // code path -- first-boot == reload) // Phase D: init() -- plugin runs startup logic with full services // Phase E: start() -- plugin launches its threads / accept loops // // This callback is optional (may be nullptr). Plugins that leave it null // keep the pre-existing two-phase behavior: Phase B is skipped and the // plugin's init() is responsible for both schema registration and startup // work (the mysqlx plugin does this today). using proxysql_plugin_register_schemas_cb = bool (*)(ProxySQL_PluginServices *); #endif /* PROXYSQL40 */ struct ProxySQL_PluginDescriptor { const char *name; uint32_t abi_version; proxysql_plugin_init_cb init; proxysql_plugin_start_cb start; proxysql_plugin_stop_cb stop; proxysql_plugin_status_json_cb status_json; #ifdef PROXYSQL40 /* Optional: called between load() and init(). * `services` will have register_table available but DB handle getters * (get_admindb/get_configdb/get_statsdb) will return nullptr. Plugins * that use this callback MUST NOT touch DB handles here; they can in * init() after admin module bootstrap materializes schema. Plugins that * leave this field null keep the pre-existing two-phase behavior. */ proxysql_plugin_register_schemas_cb register_schemas; #endif /* PROXYSQL40 */ }; using proxysql_plugin_descriptor_v1_t = const ProxySQL_PluginDescriptor *(*)(); // --------------------------------------------------------------------------- // ABI guidance: separation of duties between Admin and the plugin module. // // Admin owns the editable, persistent configuration tables (e.g. // mysqlx_users, mysqlx_routes). The plugin module owns the runtime // state — typically an in-memory snapshot kept under its own mutex. // The runtime_
in admin_db is NOT module storage; it is an // Admin-side *view* of module state, projected on demand. // // LOAD TO RUNTIME callbacks should: // - read the editable admin table directly, // - hand the rows to the module via a typed install API that swaps // state under the module's own lock, // - NOT touch runtime_ at all. // // SAVE [FROM RUNTIME] TO MEMORY callbacks should: // - dump the module's in-memory state, // - REPLACE INTO the editable admin table, // - NOT read runtime_. // // The runtime_ view is repopulated by a refresh callback registered // via services.register_runtime_view(...). Admin invokes the callback // before any SELECT against the registered table; the callback wipes // the table and re-projects from the module's in-memory state. This // matches the canonical MySQL_Authentication / runtime_mysql_users // pattern in core (lib/ProxySQL_Admin.cpp::save_mysql_users_runtime_to_ // database). It does duplicate the data briefly (in module + in the // projected admin table during a query), and that is the point: the // module is isolated from Admin's persistence, and operators see a // faithful snapshot whenever they query. // // Disk-tier copies (LOAD X FROM DISK, SAVE X TO DISK) DO copy between // configdb and admindb persistent tables and remain a plain // BEGIN/DELETE/INSERT/COMMIT — there is no module involvement and no // view projection on that path. For those copies, the empty-source- // must-still-clear-destination rule still applies (see PR #5643): run // the DELETE+INSERT unconditionally inside a single transaction and // check each execute() return. // --------------------------------------------------------------------------- #endif /* PROXYSQL40 (file-wide) */ #endif /* PROXYSQL_PLUGIN_H */