mirror of https://github.com/sysown/proxysql
Doc-accuracy review found that proxysql_refresh_configured_plugin_
runtime_views was unreachable for plugin-registered tables. The call
was placed inside
if (refresh==true) { // ProxySQL_Admin.cpp:1637
...
if (admin) {
// existing core runtime_* refreshes
proxysql_refresh_configured_plugin_runtime_views(...)
}
}
`refresh` only gets set to true by hardcoded substring matches
against core's own table names (lines 1358-1634: runtime_mysql_users,
runtime_mysql_servers, runtime_mysql_query_rules, etc.). None of
those substrings match runtime_mysqlx_* (or any other plugin-
registered view), so on a bare
SELECT * FROM runtime_mysqlx_users
the gate was false, my dispatch was skipped, the table was empty
(or stale from a previous query that DID match a core substring),
and the SELECT returned wrong data. The whole "on-demand projection"
mechanism the previous commits documented was broken for the entry
case. Issue #5687 / PR #5688.
The fix is one-line structurally: hoist the dispatch out of the
`if (refresh==true)` block and place it right after the substring-
detection section, gated only on `if (admin)`. The chassis
dispatcher itself (refresh_runtime_views_for_query in
ProxySQL_PluginManager.cpp) already does its own per-view substring
match against query_no_space, so a query that touches no registered
view is a cheap no-op (one shared lock + N substring scans, N ==
registered-view count). Calling unconditionally on every admin
query is therefore both correct and cheap.
Test: new plugin_runtime_views_unit-t (20 ok asserts) drives
ProxySQL_PluginManager::register_runtime_view +
refresh_runtime_views_for_query directly. Covers:
- register_runtime_view rejects null callback / empty name /
case-insensitive duplicate.
- Per-query dispatch fan-out: only matching callbacks fire,
join queries fire all referenced views, unrelated queries
fire nothing, case-insensitive match works, backtick-quoted
identifiers match.
- Whole-identifier boundary: longer-suffix overlap (runtime_
mysqlx_users_extra), left-prefix overlap (stats_runtime_
mysqlx_users), embedded-in-string-literal — none falsely
match. Boundary cases (start of string, end of string) do
match.
This is the test the PR-#5688 review pass identified as the chassis-
hook coverage gap. Builds standalone (no fake-plugin loader needed)
since it drives the manager directly.
fix/mysqlx-runtime-views-separation
parent
b4127156ed
commit
90e888e1d0
@ -0,0 +1,170 @@
|
||||
// Unit tests for the chassis runtime-view registration + dispatch added in
|
||||
// PR #5688: ProxySQL_PluginManager::register_runtime_view and
|
||||
// refresh_runtime_views_for_query.
|
||||
//
|
||||
// This is the only test that drives the chassis surface directly without
|
||||
// the plugin loader, so it can exercise:
|
||||
// - registration rejection (null cb, empty name, duplicate)
|
||||
// - case-insensitive whole-identifier substring match
|
||||
// - per-query dispatch fan-out (only matching callbacks fire)
|
||||
// - no-op for queries that reference no registered view
|
||||
//
|
||||
// Without these tests a regression that, say, replaced the careful
|
||||
// whole-identifier match with a plain strstr() would silently start firing
|
||||
// the wrong projection callback on substring-overlap table names.
|
||||
|
||||
#include "tap.h"
|
||||
#include "test_globals.h"
|
||||
#include "test_init.h"
|
||||
#include "ProxySQL_PluginManager.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct CallbackProbe {
|
||||
const char* tag;
|
||||
std::atomic<int>* counter;
|
||||
};
|
||||
|
||||
void probe_refresh_cb(SQLite3DB* /*admindb*/, void* opaque) {
|
||||
auto* probe = static_cast<CallbackProbe*>(opaque);
|
||||
if (probe && probe->counter) probe->counter->fetch_add(1);
|
||||
}
|
||||
|
||||
void noop_cb(SQLite3DB*, void*) {}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
setvbuf(stdout, nullptr, _IOLBF, 0);
|
||||
plan(20);
|
||||
diag("=== plugin_runtime_views_unit-t starting ===");
|
||||
|
||||
// ---- Registration validation ----
|
||||
|
||||
{
|
||||
diag(">>> register_runtime_view rejects null callback / empty name / duplicate");
|
||||
ProxySQL_PluginManager mgr;
|
||||
|
||||
ok(mgr.register_runtime_view({nullptr, &noop_cb, nullptr}) == false,
|
||||
"register_runtime_view rejects null table_name");
|
||||
ok(mgr.register_runtime_view({"", &noop_cb, nullptr}) == false,
|
||||
"register_runtime_view rejects empty table_name");
|
||||
ok(mgr.register_runtime_view({"runtime_x", nullptr, nullptr}) == false,
|
||||
"register_runtime_view rejects null refresh callback");
|
||||
|
||||
ok(mgr.register_runtime_view({"runtime_x", &noop_cb, nullptr}) == true,
|
||||
"first registration of runtime_x succeeds");
|
||||
ok(mgr.register_runtime_view({"runtime_x", &noop_cb, nullptr}) == false,
|
||||
"duplicate registration of runtime_x is rejected");
|
||||
ok(mgr.register_runtime_view({"RUNTIME_X", &noop_cb, nullptr}) == false,
|
||||
"duplicate registration with different case is also rejected");
|
||||
}
|
||||
|
||||
// ---- Dispatch: matching queries fire the callback ----
|
||||
|
||||
{
|
||||
diag(">>> refresh_runtime_views_for_query fires only matching callbacks");
|
||||
|
||||
std::atomic<int> users_fires{0};
|
||||
std::atomic<int> routes_fires{0};
|
||||
CallbackProbe users_probe{"users", &users_fires};
|
||||
CallbackProbe routes_probe{"routes", &routes_fires};
|
||||
|
||||
ProxySQL_PluginManager mgr;
|
||||
ok(mgr.register_runtime_view({"runtime_mysqlx_users", &probe_refresh_cb, &users_probe}) == true,
|
||||
"registered runtime_mysqlx_users callback");
|
||||
ok(mgr.register_runtime_view({"runtime_mysqlx_routes", &probe_refresh_cb, &routes_probe}) == true,
|
||||
"registered runtime_mysqlx_routes callback");
|
||||
|
||||
// Query references only one of the registered views.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT * FROM runtime_mysqlx_users WHERE active=1", nullptr);
|
||||
ok(users_fires.load() == 1 && routes_fires.load() == 0,
|
||||
"users-only query fires users (got %d) and not routes (got %d)",
|
||||
users_fires.load(), routes_fires.load());
|
||||
|
||||
// Query references both.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT u.username FROM runtime_mysqlx_users u JOIN runtime_mysqlx_routes r ON u.default_route=r.name",
|
||||
nullptr);
|
||||
ok(users_fires.load() == 2 && routes_fires.load() == 1,
|
||||
"join query fires both (users=%d, routes=%d)",
|
||||
users_fires.load(), routes_fires.load());
|
||||
|
||||
// Query references neither.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT * FROM mysql_users", nullptr);
|
||||
ok(users_fires.load() == 2 && routes_fires.load() == 1,
|
||||
"unrelated query fires nothing (users=%d, routes=%d)",
|
||||
users_fires.load(), routes_fires.load());
|
||||
|
||||
// Case-insensitive match on the table name.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT * FROM RUNTIME_MYSQLX_USERS", nullptr);
|
||||
ok(users_fires.load() == 3,
|
||||
"uppercase table name still matches (users=%d)", users_fires.load());
|
||||
|
||||
// Backtick-quoted identifier — backtick is not an identifier char so
|
||||
// the whole-identifier match still succeeds.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT * FROM `runtime_mysqlx_users`", nullptr);
|
||||
ok(users_fires.load() == 4,
|
||||
"backtick-quoted identifier still matches (users=%d)", users_fires.load());
|
||||
}
|
||||
|
||||
// ---- Whole-identifier match: prefix and suffix overlaps must NOT match ----
|
||||
|
||||
{
|
||||
diag(">>> sql_references_table_ci respects identifier boundaries");
|
||||
|
||||
std::atomic<int> fires{0};
|
||||
CallbackProbe probe{"x", &fires};
|
||||
|
||||
ProxySQL_PluginManager mgr;
|
||||
ok(mgr.register_runtime_view({"runtime_mysqlx_users", &probe_refresh_cb, &probe}) == true,
|
||||
"registered runtime_mysqlx_users callback");
|
||||
|
||||
// Substring overlap that's part of a longer identifier — must NOT match.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT * FROM runtime_mysqlx_users_extra", nullptr);
|
||||
ok(fires.load() == 0,
|
||||
"longer-identifier overlap does not match (fires=%d)", fires.load());
|
||||
|
||||
// Suffix overlap.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT * FROM stats_runtime_mysqlx_users", nullptr);
|
||||
ok(fires.load() == 0,
|
||||
"left-side identifier prefix does not match (fires=%d)", fires.load());
|
||||
|
||||
// Embedded inside a longer word, no boundary on either side.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT 'xruntime_mysqlx_usersy' FROM dual", nullptr);
|
||||
ok(fires.load() == 0,
|
||||
"embedded-in-string-literal does not match (fires=%d)", fires.load());
|
||||
|
||||
// Real reference: should match.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"SELECT * FROM runtime_mysqlx_users", nullptr);
|
||||
ok(fires.load() == 1,
|
||||
"exact identifier match fires the callback (fires=%d)", fires.load());
|
||||
|
||||
// Identifier at end of string (no trailing whitespace).
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"DESC runtime_mysqlx_users", nullptr);
|
||||
ok(fires.load() == 2,
|
||||
"identifier at end-of-string still matches (fires=%d)", fires.load());
|
||||
|
||||
// Identifier at start of string.
|
||||
mgr.refresh_runtime_views_for_query(
|
||||
"runtime_mysqlx_users", nullptr);
|
||||
ok(fires.load() == 3,
|
||||
"identifier at start-of-string still matches (fires=%d)", fires.load());
|
||||
}
|
||||
|
||||
return exit_status();
|
||||
}
|
||||
Loading…
Reference in new issue