mirror of https://github.com/sysown/proxysql
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.
297 lines
12 KiB
297 lines
12 KiB
// Step 2.2 (chassis): four-phase plugin lifecycle test.
|
|
//
|
|
// Exercises the new register_schemas (Phase B) ABI field and verifies:
|
|
// * both register_schemas and init run when the plugin sets both;
|
|
// register_schemas runs first, init second (ordering contract).
|
|
// * plugins with a null register_schemas field skip Phase B and go
|
|
// straight to init (pre-existing two-phase behavior preserved).
|
|
// * during Phase B, the DB handle getters in services return nullptr.
|
|
// Plugins that call them observe null and must handle it.
|
|
//
|
|
// The fake_plugin.cpp test helper is the plugin under test; it selects
|
|
// between two static ProxySQL_PluginDescriptor layouts based on the
|
|
// PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B env var, mirroring the descriptor-
|
|
// version-selection pattern a real plugin would use.
|
|
|
|
#include "ProxySQL_PluginManager.h"
|
|
#include "ProxySQL_Plugin.h"
|
|
#include "tap.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
#ifndef PROXYSQL_FAKE_PLUGIN_PATH
|
|
#error "PROXYSQL_FAKE_PLUGIN_PATH must be defined"
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
char g_fake_admin_db = '\0';
|
|
char g_fake_config_db = '\0';
|
|
char g_fake_stats_db = '\0';
|
|
|
|
std::string g_log_path {};
|
|
|
|
void make_log_path() {
|
|
char tpl[] = "/tmp/proxysql_plugin_lifecycle_log.XXXXXX";
|
|
int fd = mkstemp(tpl);
|
|
if (fd >= 0) close(fd);
|
|
g_log_path = tpl;
|
|
setenv("PROXYSQL_FAKE_PLUGIN_LOG", g_log_path.c_str(), 1);
|
|
}
|
|
|
|
void clear_log() {
|
|
if (g_log_path.empty()) return;
|
|
// See plugin_manager_unit-t.cpp for the SonarCloud rationale.
|
|
std::ofstream truncate_handle(g_log_path, std::ios::trunc);
|
|
(void)truncate_handle;
|
|
}
|
|
|
|
std::string read_log() {
|
|
if (g_log_path.empty()) return "";
|
|
std::ifstream s(g_log_path);
|
|
return std::string((std::istreambuf_iterator<char>(s)), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
void cleanup_log() {
|
|
if (g_log_path.empty()) return;
|
|
std::remove(g_log_path.c_str());
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_LOG");
|
|
}
|
|
|
|
bool contains_in_order(const std::string& haystack, const char* first, const char* second) {
|
|
size_t pos_first = haystack.find(first);
|
|
if (pos_first == std::string::npos) return false;
|
|
size_t pos_second = haystack.find(second, pos_first + 1);
|
|
return pos_second != std::string::npos;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Symbols the plugin manager pulls from the core. In this test harness we
|
|
// hand it fake, non-null pointers so init() sees "live" handles -- which
|
|
// is what matters for the contrast against Phase B's nullptrs.
|
|
SQLite3DB* proxysql_plugin_get_admindb() { return reinterpret_cast<SQLite3DB*>(&g_fake_admin_db); }
|
|
SQLite3DB* proxysql_plugin_get_configdb() { return reinterpret_cast<SQLite3DB*>(&g_fake_config_db); }
|
|
SQLite3DB* proxysql_plugin_get_statsdb() { return reinterpret_cast<SQLite3DB*>(&g_fake_stats_db); }
|
|
|
|
// Case 1: plugin sets both register_schemas AND init. Both must fire.
|
|
// register_schemas must fire strictly before init (the lifecycle contract
|
|
// that lets plugins count on admin module materialization between them).
|
|
static void test_phase_b_and_init_both_fire() {
|
|
setenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B", "1", 1);
|
|
clear_log();
|
|
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(proxysql_load_configured_plugins(mgr, paths, err) && proxysql_init_configured_plugins(mgr.get(), err),
|
|
"load-then-register_schemas-then-init succeeds (err='%s')", err.c_str());
|
|
|
|
const std::string log = read_log();
|
|
ok(log.find("fake_plugin:phase_b") != std::string::npos,
|
|
"register_schemas callback fired (log contains phase_b marker)");
|
|
ok(log.find("fake_plugin:init") != std::string::npos,
|
|
"init callback fired (log contains init marker)");
|
|
ok(contains_in_order(log, "fake_plugin:phase_b", "fake_plugin:init"),
|
|
"register_schemas ran strictly before init");
|
|
|
|
(void)proxysql_stop_configured_plugins(mgr, err);
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B");
|
|
}
|
|
|
|
// Case 2: plugin sets only init (register_schemas field is null).
|
|
// Phase B is skipped; init still runs. This is the compat path for
|
|
// plugins built against the pre-2.2 descriptor layout.
|
|
static void test_only_init_skips_phase_b() {
|
|
// ENABLE_PHASE_B unset -> descriptor returned omits register_schemas.
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B");
|
|
clear_log();
|
|
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(proxysql_load_configured_plugins(mgr, paths, err) && proxysql_init_configured_plugins(mgr.get(), err),
|
|
"load+init succeeds with no register_schemas set (err='%s')", err.c_str());
|
|
|
|
const std::string log = read_log();
|
|
ok(log.find("fake_plugin:phase_b") == std::string::npos,
|
|
"register_schemas NOT invoked when descriptor field is null");
|
|
ok(log.find("fake_plugin:init") != std::string::npos,
|
|
"init still fires on the legacy two-phase path");
|
|
|
|
(void)proxysql_stop_configured_plugins(mgr, err);
|
|
}
|
|
|
|
// Case 3: register_schemas tries to call DB handle getters.
|
|
//
|
|
// Contract: during Phase B the services struct passed to the plugin MUST
|
|
// have the DB-handle getters wired to non-null stub functions that return
|
|
// nullptr. Two regressions this test has to catch:
|
|
// (a) loader passes the live `services_` (get_admindb() returns the
|
|
// real, non-null admin DB)
|
|
// (b) loader sets services_phase_b_.get_admindb = nullptr (plugins that
|
|
// call the getter unconditionally would crash)
|
|
//
|
|
// The fake plugin emits one of three markers depending on which state it
|
|
// observes; the correct marker is phase_b_handles_null. This test only
|
|
// passes when the marker is present AND the two failure markers are
|
|
// absent. The harness itself returns non-null fakes from
|
|
// proxysql_plugin_get_admindb(); if the loader mistakenly used the live
|
|
// services, the fake plugin would log phase_b_handles_live.
|
|
static void test_phase_b_db_handles_are_null() {
|
|
setenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B", "1", 1);
|
|
setenv("PROXYSQL_FAKE_PLUGIN_PHASE_B_TOUCH_HANDLES", "1", 1);
|
|
clear_log();
|
|
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(proxysql_load_configured_plugins(mgr, paths, err) && proxysql_init_configured_plugins(mgr.get(), err),
|
|
"load succeeds even though plugin peeked at DB handles in Phase B");
|
|
|
|
const std::string log = read_log();
|
|
ok(log.find("fake_plugin:phase_b_handles_null") != std::string::npos,
|
|
"DB handle getters returned nullptr during register_schemas (log='%s')",
|
|
log.c_str());
|
|
ok(log.find("fake_plugin:phase_b_handles_live") == std::string::npos,
|
|
"DB handles were NOT live during Phase B (contract)");
|
|
ok(log.find("fake_plugin:phase_b_getter_null") == std::string::npos,
|
|
"Phase-B getters are non-null stubs, not nullptr pointers (contract)");
|
|
|
|
(void)proxysql_stop_configured_plugins(mgr, err);
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_PHASE_B_TOUCH_HANDLES");
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B");
|
|
}
|
|
|
|
// Case 4: a failing register_schemas aborts the load and init() is NOT
|
|
// called. Verifies the loader treats Phase-B failure as a hard error.
|
|
static void test_phase_b_failure_aborts_init() {
|
|
setenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B", "1", 1);
|
|
setenv("PROXYSQL_FAKE_PLUGIN_PHASE_B_FAIL", "1", 1);
|
|
clear_log();
|
|
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(!proxysql_load_configured_plugins(mgr, paths, err),
|
|
"load fails when register_schemas returns false");
|
|
ok(!err.empty() && err.find("register_schemas") != std::string::npos,
|
|
"error message names the failing phase (err='%s')", err.c_str());
|
|
|
|
const std::string log = read_log();
|
|
ok(log.find("fake_plugin:phase_b_fail") != std::string::npos,
|
|
"register_schemas actually ran and logged its failure");
|
|
ok(log.find("fake_plugin:init") == std::string::npos,
|
|
"init was NOT called after register_schemas failed");
|
|
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_PHASE_B_FAIL");
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B");
|
|
}
|
|
|
|
// Case 4b: register_schemas registers a table, then returns false.
|
|
// The loader MUST roll back the partial registration so a subsequent
|
|
// retry (with the bug fixed) doesn't trip on a duplicate table
|
|
// registration. This is the contract that keeps reload-after-failure
|
|
// viable: the registry is transactional per-plugin.
|
|
static void test_phase_b_partial_failure_rolls_back() {
|
|
setenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B", "1", 1);
|
|
setenv("PROXYSQL_FAKE_PLUGIN_PHASE_B_PARTIAL_THEN_FAIL", "1", 1);
|
|
clear_log();
|
|
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr1;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(!proxysql_load_configured_plugins(mgr1, paths, err),
|
|
"first load fails when register_schemas returns false after partial registration");
|
|
ok(read_log().find("fake_plugin:phase_b_partial_then_fail") != std::string::npos,
|
|
"plugin actually registered a table before returning false");
|
|
|
|
// Retry with the toggle cleared. If the loader didn't roll back
|
|
// the partial registration, we'd expect either a duplicate-table
|
|
// error or a dirty registry. With rollback, load+init succeeds.
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_PHASE_B_PARTIAL_THEN_FAIL");
|
|
clear_log();
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr2;
|
|
std::string err2;
|
|
ok(proxysql_load_configured_plugins(mgr2, paths, err2) && proxysql_init_configured_plugins(mgr2.get(), err2),
|
|
"retry succeeds — partial registration from the failed attempt was rolled back (err='%s')", err2.c_str());
|
|
|
|
(void)proxysql_stop_configured_plugins(mgr2, err2);
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_ENABLE_PHASE_B");
|
|
}
|
|
|
|
// Case 5: init() succeeds but start() fails. stop() MUST still be
|
|
// called for teardown symmetry — anything init() allocated would otherwise
|
|
// leak. This is the "init pairs with stop" contract.
|
|
static void test_stop_runs_when_start_fails() {
|
|
setenv("PROXYSQL_FAKE_PLUGIN_START_FAIL", "1", 1);
|
|
clear_log();
|
|
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(proxysql_load_configured_plugins(mgr, paths, err) && proxysql_init_configured_plugins(mgr.get(), err),
|
|
"load + init succeed on the plugin whose start will later fail");
|
|
ok(!proxysql_start_configured_plugins(mgr.get(), err),
|
|
"start fails when plugin's start() returns false");
|
|
|
|
(void)proxysql_stop_configured_plugins(mgr, err);
|
|
|
|
const std::string log = read_log();
|
|
ok(log.find("fake_plugin:init") != std::string::npos,
|
|
"init did run (necessary precondition for the stop contract)");
|
|
ok(log.find("fake_plugin:start_fail") != std::string::npos,
|
|
"start_fail marker confirms start() was called and returned false");
|
|
ok(log.find("fake_plugin:stop") != std::string::npos,
|
|
"stop() was called for init-success/start-fail plugin (teardown symmetry)");
|
|
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_START_FAIL");
|
|
}
|
|
|
|
// Case 6: plugin returns a descriptor with an unknown abi_version. The
|
|
// loader MUST refuse to load such a plugin rather than read past the end
|
|
// of its own (compiled-against) struct definition. This is the test that
|
|
// keeps the tail-append pattern honest across plugin/core ABI skew.
|
|
static void test_bogus_abi_version_rejected() {
|
|
setenv("PROXYSQL_FAKE_PLUGIN_FORCE_BOGUS_ABI", "1", 1);
|
|
clear_log();
|
|
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(!proxysql_load_configured_plugins(mgr, paths, err),
|
|
"load fails when plugin declares an unsupported ABI version");
|
|
ok(!err.empty() && err.find("ABI") != std::string::npos,
|
|
"error message names the ABI mismatch (err='%s')", err.c_str());
|
|
|
|
const std::string log = read_log();
|
|
ok(log.find("fake_plugin:init") == std::string::npos,
|
|
"init was NOT called on a plugin rejected by the ABI check");
|
|
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_FORCE_BOGUS_ABI");
|
|
}
|
|
|
|
int main() {
|
|
plan(26);
|
|
|
|
make_log_path();
|
|
|
|
test_phase_b_and_init_both_fire();
|
|
test_only_init_skips_phase_b();
|
|
test_phase_b_db_handles_are_null();
|
|
test_phase_b_failure_aborts_init();
|
|
test_phase_b_partial_failure_rolls_back();
|
|
test_stop_runs_when_start_fails();
|
|
test_bogus_abi_version_rejected();
|
|
|
|
cleanup_log();
|
|
return exit_status();
|
|
}
|