mirror of https://github.com/sysown/proxysql
Signed-off-by: René Cannaò <rene@proxysql.com>pull/5513/head
commit
8d7459e560
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @file BackendSyncDecision.h
|
||||
* @brief Pure decision functions for backend variable synchronization.
|
||||
*
|
||||
* Extracted from MySQL_Session's verify chain (handler_again___verify_*).
|
||||
* Determines what sync actions are needed before a query can execute
|
||||
* on a backend connection.
|
||||
*
|
||||
* @see Phase 3.6 (GitHub issue #5494)
|
||||
*/
|
||||
|
||||
#ifndef BACKEND_SYNC_DECISION_H
|
||||
#define BACKEND_SYNC_DECISION_H
|
||||
|
||||
/**
|
||||
* @brief Actions that may be needed to synchronize backend state.
|
||||
*/
|
||||
enum BackendSyncAction {
|
||||
SYNC_NONE = 0, ///< No synchronization needed.
|
||||
SYNC_SCHEMA = 1, ///< Schema (USE db) needs to be sent.
|
||||
SYNC_USER = 2, ///< Username mismatch, CHANGE USER required.
|
||||
SYNC_AUTOCOMMIT = 4, ///< Autocommit state needs to be synced.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Determine what sync actions are needed for the backend.
|
||||
*
|
||||
* Checks client vs backend state and returns a bitmask of required
|
||||
* actions. Mirrors the MySQL_Session verify chain logic.
|
||||
*
|
||||
* @param client_user Client connection username.
|
||||
* @param backend_user Backend connection username.
|
||||
* @param client_schema Client connection schema.
|
||||
* @param backend_schema Backend connection schema.
|
||||
* @param client_autocommit Client autocommit setting.
|
||||
* @param backend_autocommit Backend autocommit setting.
|
||||
* @return Bitmask of BackendSyncAction values.
|
||||
*/
|
||||
int determine_backend_sync_actions(
|
||||
const char *client_user,
|
||||
const char *backend_user,
|
||||
const char *client_schema,
|
||||
const char *backend_schema,
|
||||
bool client_autocommit,
|
||||
bool backend_autocommit
|
||||
);
|
||||
|
||||
#endif // BACKEND_SYNC_DECISION_H
|
||||
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @file MySQLErrorClassifier.h
|
||||
* @brief Pure MySQL error classification for retry decisions.
|
||||
*
|
||||
* Extracted from MySQL_Session handler_ProcessingQueryError_CheckBackendConnectionStatus()
|
||||
* and handler_minus1_HandleErrorCodes().
|
||||
*
|
||||
* @see Phase 3.7 (GitHub issue #5495)
|
||||
*/
|
||||
|
||||
#ifndef MYSQL_ERROR_CLASSIFIER_H
|
||||
#define MYSQL_ERROR_CLASSIFIER_H
|
||||
|
||||
/**
|
||||
* @brief Action to take after a MySQL backend query error.
|
||||
*/
|
||||
enum MySQLErrorAction {
|
||||
MYSQL_ERROR_RETRY_ON_NEW_CONN, ///< Reconnect and retry on a new server.
|
||||
MYSQL_ERROR_REPORT_TO_CLIENT ///< Send error to client, no retry.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Classify a MySQL error code to determine retry eligibility.
|
||||
*
|
||||
* Mirrors the logic in handler_minus1_HandleErrorCodes():
|
||||
* - Error 1047 (WSREP not ready): retryable if conditions permit
|
||||
* - Error 1053 (server shutdown): retryable if conditions permit
|
||||
* - Other errors: report to client
|
||||
*
|
||||
* Retry is only possible when:
|
||||
* - query_retries_on_failure > 0
|
||||
* - connection is reusable
|
||||
* - no active transaction
|
||||
* - multiplex not disabled
|
||||
*
|
||||
* @param error_code MySQL error number.
|
||||
* @param retries_remaining Number of retries left (> 0 to allow retry).
|
||||
* @param connection_reusable Whether the connection can be reused.
|
||||
* @param in_active_transaction Whether a transaction is in progress.
|
||||
* @param multiplex_disabled Whether multiplexing is disabled.
|
||||
* @return MySQLErrorAction indicating what to do.
|
||||
*/
|
||||
MySQLErrorAction classify_mysql_error(
|
||||
unsigned int error_code,
|
||||
int retries_remaining,
|
||||
bool connection_reusable,
|
||||
bool in_active_transaction,
|
||||
bool multiplex_disabled
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Check if a backend query can be retried on a new connection.
|
||||
*
|
||||
* Mirrors handler_ProcessingQueryError_CheckBackendConnectionStatus().
|
||||
* A retry is possible when the server is offline AND all retry
|
||||
* conditions are met.
|
||||
*
|
||||
* @param server_offline Whether the backend server is offline.
|
||||
* @param retries_remaining Number of retries left (> 0 to allow retry).
|
||||
* @param connection_reusable Whether the connection can be reused.
|
||||
* @param in_active_transaction Whether a transaction is in progress.
|
||||
* @param multiplex_disabled Whether multiplexing is disabled.
|
||||
* @param transfer_started Whether result transfer has already begun.
|
||||
* @return true if the query should be retried on a new connection.
|
||||
*/
|
||||
bool can_retry_on_new_connection(
|
||||
bool server_offline,
|
||||
int retries_remaining,
|
||||
bool connection_reusable,
|
||||
bool in_active_transaction,
|
||||
bool multiplex_disabled,
|
||||
bool transfer_started
|
||||
);
|
||||
|
||||
#endif // MYSQL_ERROR_CLASSIFIER_H
|
||||
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @file ServerSelection.h
|
||||
* @brief Pure server selection algorithm for unit testability.
|
||||
*
|
||||
* Extracted from get_random_MySrvC() in the HostGroups Manager.
|
||||
* Uses a lightweight ServerCandidate struct instead of MySrvC to
|
||||
* avoid connection pool dependencies.
|
||||
*
|
||||
* @see Phase 3.4 (GitHub issue #5492)
|
||||
*/
|
||||
|
||||
#ifndef SERVER_SELECTION_H
|
||||
#define SERVER_SELECTION_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* @brief Server status values (mirrors MySerStatus enum).
|
||||
*
|
||||
* Redefined here to avoid pulling in proxysql_structs.h and its
|
||||
* entire dependency chain.
|
||||
*/
|
||||
enum ServerSelectionStatus {
|
||||
SERVER_ONLINE = 0,
|
||||
SERVER_SHUNNED = 1,
|
||||
SERVER_OFFLINE_SOFT = 2,
|
||||
SERVER_OFFLINE_HARD = 3,
|
||||
SERVER_SHUNNED_REPLICATION_LAG = 4
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lightweight struct with decision-relevant server fields only.
|
||||
*
|
||||
* Avoids coupling to MySrvC which contains connection pool pointers,
|
||||
* MySQL_Connection objects, and other heavy dependencies.
|
||||
*/
|
||||
struct ServerCandidate {
|
||||
int index; ///< Caller-defined index (returned on selection).
|
||||
int64_t weight; ///< Selection weight (0 = never selected).
|
||||
ServerSelectionStatus status; ///< Current health status.
|
||||
unsigned int current_connections; ///< Active connection count.
|
||||
unsigned int max_connections; ///< Maximum allowed connections.
|
||||
unsigned int current_latency_us; ///< Measured latency in microseconds.
|
||||
unsigned int max_latency_us; ///< Maximum allowed latency (0 = no limit).
|
||||
unsigned int current_repl_lag; ///< Measured replication lag in seconds.
|
||||
unsigned int max_repl_lag; ///< Maximum allowed lag (0 = no limit).
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Check if a server candidate is eligible for selection.
|
||||
*
|
||||
* A candidate is eligible when:
|
||||
* - status == SERVER_ONLINE
|
||||
* - current_connections < max_connections
|
||||
* - current_latency_us <= max_latency_us (or max_latency_us == 0)
|
||||
* - current_repl_lag <= max_repl_lag (or max_repl_lag == 0)
|
||||
*
|
||||
* @note In production, max_latency_us == 0 on a per-server basis means
|
||||
* "use the thread default max latency." This extraction treats 0
|
||||
* as "no limit" for simplicity. Callers should resolve defaults
|
||||
* before populating the ServerCandidate.
|
||||
*
|
||||
* @return true if the candidate is eligible.
|
||||
*/
|
||||
bool is_candidate_eligible(const ServerCandidate &candidate);
|
||||
|
||||
/**
|
||||
* @brief Select a server from candidates using weighted random selection.
|
||||
*
|
||||
* Filters candidates by eligibility, then selects from eligible ones
|
||||
* using weighted random with the provided seed for deterministic testing.
|
||||
*
|
||||
* @param candidates Array of server candidates.
|
||||
* @param count Number of candidates in the array.
|
||||
* @param random_seed Seed for deterministic random selection.
|
||||
* @return Index field of the selected candidate, or -1 if none eligible.
|
||||
*/
|
||||
int select_server_from_candidates(
|
||||
const ServerCandidate *candidates,
|
||||
int count,
|
||||
unsigned int random_seed
|
||||
);
|
||||
|
||||
#endif // SERVER_SELECTION_H
|
||||
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @file BackendSyncDecision.cpp
|
||||
* @brief Implementation of backend variable sync decisions.
|
||||
*
|
||||
* @see BackendSyncDecision.h
|
||||
* @see Phase 3.6 (GitHub issue #5494)
|
||||
*/
|
||||
|
||||
#include "BackendSyncDecision.h"
|
||||
#include <cstring>
|
||||
|
||||
int determine_backend_sync_actions(
|
||||
const char *client_user,
|
||||
const char *backend_user,
|
||||
const char *client_schema,
|
||||
const char *backend_schema,
|
||||
bool client_autocommit,
|
||||
bool backend_autocommit)
|
||||
{
|
||||
int actions = SYNC_NONE;
|
||||
|
||||
// Username mismatch → CHANGE USER required
|
||||
// Asymmetric NULLs (one set, other not) count as mismatch
|
||||
if (client_user == nullptr && backend_user != nullptr) {
|
||||
actions |= SYNC_USER;
|
||||
} else if (client_user != nullptr && backend_user == nullptr) {
|
||||
actions |= SYNC_USER;
|
||||
} else if (client_user && backend_user) {
|
||||
if (strcmp(client_user, backend_user) != 0) {
|
||||
actions |= SYNC_USER;
|
||||
}
|
||||
}
|
||||
|
||||
// Schema mismatch → USE <db> required
|
||||
// Only check if usernames match (user change handles schema too)
|
||||
if (!(actions & SYNC_USER)) {
|
||||
if (client_schema == nullptr && backend_schema != nullptr) {
|
||||
actions |= SYNC_SCHEMA;
|
||||
} else if (client_schema != nullptr && backend_schema == nullptr) {
|
||||
actions |= SYNC_SCHEMA;
|
||||
} else if (client_schema && backend_schema) {
|
||||
if (strcmp(client_schema, backend_schema) != 0) {
|
||||
actions |= SYNC_SCHEMA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Autocommit mismatch → SET autocommit required
|
||||
if (client_autocommit != backend_autocommit) {
|
||||
actions |= SYNC_AUTOCOMMIT;
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file MySQLErrorClassifier.cpp
|
||||
* @brief Implementation of MySQL error classification.
|
||||
*
|
||||
* @see MySQLErrorClassifier.h
|
||||
* @see Phase 3.7 (GitHub issue #5495)
|
||||
*/
|
||||
|
||||
#include "MySQLErrorClassifier.h"
|
||||
|
||||
MySQLErrorAction classify_mysql_error(
|
||||
unsigned int error_code,
|
||||
int retries_remaining,
|
||||
bool connection_reusable,
|
||||
bool in_active_transaction,
|
||||
bool multiplex_disabled)
|
||||
{
|
||||
// Check if this error code is retryable
|
||||
bool retryable_error = false;
|
||||
switch (error_code) {
|
||||
case 1047: // ER_UNKNOWN_COM_ERROR (WSREP not ready)
|
||||
case 1053: // ER_SERVER_SHUTDOWN
|
||||
retryable_error = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!retryable_error) {
|
||||
return MYSQL_ERROR_REPORT_TO_CLIENT;
|
||||
}
|
||||
|
||||
// Check retry conditions (mirrors handler_minus1_HandleErrorCodes)
|
||||
if (retries_remaining > 0
|
||||
&& connection_reusable
|
||||
&& !in_active_transaction
|
||||
&& !multiplex_disabled) {
|
||||
return MYSQL_ERROR_RETRY_ON_NEW_CONN;
|
||||
}
|
||||
|
||||
return MYSQL_ERROR_REPORT_TO_CLIENT;
|
||||
}
|
||||
|
||||
bool can_retry_on_new_connection(
|
||||
bool server_offline,
|
||||
int retries_remaining,
|
||||
bool connection_reusable,
|
||||
bool in_active_transaction,
|
||||
bool multiplex_disabled,
|
||||
bool transfer_started)
|
||||
{
|
||||
if (!server_offline) {
|
||||
return false; // server is fine, no retry needed
|
||||
}
|
||||
|
||||
// Mirror handler_ProcessingQueryError_CheckBackendConnectionStatus
|
||||
if (retries_remaining > 0
|
||||
&& connection_reusable
|
||||
&& !in_active_transaction
|
||||
&& !multiplex_disabled
|
||||
&& !transfer_started) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file ServerSelection.cpp
|
||||
* @brief Implementation of the pure server selection algorithm.
|
||||
*
|
||||
* @see ServerSelection.h
|
||||
* @see Phase 3.4 (GitHub issue #5492)
|
||||
*/
|
||||
|
||||
#include "ServerSelection.h"
|
||||
|
||||
bool is_candidate_eligible(const ServerCandidate &c) {
|
||||
if (c.status != SERVER_ONLINE) {
|
||||
return false;
|
||||
}
|
||||
if (c.current_connections >= c.max_connections) {
|
||||
return false;
|
||||
}
|
||||
if (c.max_latency_us > 0 && c.current_latency_us > c.max_latency_us) {
|
||||
return false;
|
||||
}
|
||||
if (c.max_repl_lag > 0 && c.current_repl_lag > c.max_repl_lag) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int select_server_from_candidates(
|
||||
const ServerCandidate *candidates,
|
||||
int count,
|
||||
unsigned int random_seed)
|
||||
{
|
||||
if (candidates == nullptr || count <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// First pass: compute total weight of eligible candidates
|
||||
int64_t total_weight = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (is_candidate_eligible(candidates[i]) && candidates[i].weight > 0) {
|
||||
total_weight += candidates[i].weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (total_weight == 0) {
|
||||
return -1; // no eligible candidates
|
||||
}
|
||||
|
||||
// Seeded random selection
|
||||
// Use a simple LCG to avoid polluting global srand() state
|
||||
// LCG: next = (a * seed + c) mod m (Numerical Recipes parameters)
|
||||
unsigned int rng_state = random_seed;
|
||||
rng_state = rng_state * 1664525u + 1013904223u;
|
||||
// Use 64-bit modulo to avoid truncation when total_weight > UINT_MAX
|
||||
int64_t target = (int64_t)(rng_state % (uint64_t)total_weight) + 1;
|
||||
|
||||
// Second pass: weighted selection
|
||||
int64_t cumulative = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (is_candidate_eligible(candidates[i]) && candidates[i].weight > 0) {
|
||||
cumulative += candidates[i].weight;
|
||||
if (cumulative >= target) {
|
||||
return candidates[i].index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should not reach here if total_weight > 0, but safety fallback
|
||||
return -1;
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file backend_sync_unit-t.cpp
|
||||
* @brief Unit tests for backend variable sync decisions.
|
||||
*
|
||||
* @see Phase 3.6 (GitHub issue #5494)
|
||||
*/
|
||||
|
||||
#include "tap.h"
|
||||
#include "test_globals.h"
|
||||
#include "test_init.h"
|
||||
#include "proxysql.h"
|
||||
#include "BackendSyncDecision.h"
|
||||
|
||||
static void test_no_sync_needed() {
|
||||
int a = determine_backend_sync_actions("user", "user", "db", "db", true, true);
|
||||
ok(a == SYNC_NONE, "no sync: all match");
|
||||
|
||||
a = determine_backend_sync_actions("user", "user", "db", "db", false, false);
|
||||
ok(a == SYNC_NONE, "no sync: autocommit both false");
|
||||
}
|
||||
|
||||
static void test_schema_mismatch() {
|
||||
int a = determine_backend_sync_actions("user", "user", "app_db", "other_db", true, true);
|
||||
ok((a & SYNC_SCHEMA) != 0, "schema mismatch: SYNC_SCHEMA set");
|
||||
ok((a & SYNC_USER) == 0, "schema mismatch: SYNC_USER not set");
|
||||
}
|
||||
|
||||
static void test_user_mismatch() {
|
||||
int a = determine_backend_sync_actions("alice", "bob", "db", "db", true, true);
|
||||
ok((a & SYNC_USER) != 0, "user mismatch: SYNC_USER set");
|
||||
// Schema check skipped when user differs (CHANGE USER handles schema)
|
||||
ok((a & SYNC_SCHEMA) == 0, "user mismatch: SYNC_SCHEMA not set (handled by CHANGE USER)");
|
||||
}
|
||||
|
||||
static void test_user_and_schema_mismatch() {
|
||||
int a = determine_backend_sync_actions("alice", "bob", "db1", "db2", true, true);
|
||||
ok((a & SYNC_USER) != 0, "user+schema: SYNC_USER set");
|
||||
ok((a & SYNC_SCHEMA) == 0, "user+schema: schema handled by user change");
|
||||
}
|
||||
|
||||
static void test_autocommit_mismatch() {
|
||||
int a = determine_backend_sync_actions("user", "user", "db", "db", true, false);
|
||||
ok((a & SYNC_AUTOCOMMIT) != 0, "autocommit mismatch: SYNC_AUTOCOMMIT set");
|
||||
ok((a & SYNC_SCHEMA) == 0, "autocommit mismatch: no other sync");
|
||||
}
|
||||
|
||||
static void test_multiple_mismatches() {
|
||||
int a = determine_backend_sync_actions("user", "user", "db1", "db2", true, false);
|
||||
ok((a & SYNC_SCHEMA) != 0, "multi: SYNC_SCHEMA set");
|
||||
ok((a & SYNC_AUTOCOMMIT) != 0, "multi: SYNC_AUTOCOMMIT set");
|
||||
}
|
||||
|
||||
static void test_null_handling() {
|
||||
// null users — no crash
|
||||
// Asymmetric NULL: one side null, other not → mismatch
|
||||
int a = determine_backend_sync_actions(nullptr, "user", "db", "db", true, true);
|
||||
ok((a & SYNC_USER) != 0, "null client_user + non-null backend → SYNC_USER");
|
||||
|
||||
a = determine_backend_sync_actions("user", nullptr, "db", "db", true, true);
|
||||
ok((a & SYNC_USER) != 0, "non-null client_user + null backend → SYNC_USER");
|
||||
|
||||
// Both null → no mismatch
|
||||
a = determine_backend_sync_actions(nullptr, nullptr, "db", "db", true, true);
|
||||
ok(a == SYNC_NONE, "both users null → no sync");
|
||||
|
||||
// Schema asymmetric null
|
||||
a = determine_backend_sync_actions("user", "user", nullptr, "db", true, true);
|
||||
ok((a & SYNC_SCHEMA) != 0, "null client_schema + non-null backend → SYNC_SCHEMA");
|
||||
}
|
||||
|
||||
int main() {
|
||||
plan(17);
|
||||
int rc = test_init_minimal();
|
||||
ok(rc == 0, "test_init_minimal() succeeds");
|
||||
|
||||
test_no_sync_needed(); // 2
|
||||
test_schema_mismatch(); // 2
|
||||
test_user_mismatch(); // 2
|
||||
test_user_and_schema_mismatch(); // 2
|
||||
test_autocommit_mismatch(); // 2
|
||||
test_multiple_mismatches(); // 2
|
||||
test_null_handling(); // 4
|
||||
// Total: 1+2+2+2+2+2+2+4 = 17
|
||||
|
||||
test_cleanup_minimal();
|
||||
return exit_status();
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @file mysql_error_classifier_unit-t.cpp
|
||||
* @brief Unit tests for MySQL error classification.
|
||||
*
|
||||
* @see Phase 3.7 (GitHub issue #5495)
|
||||
*/
|
||||
|
||||
#include "tap.h"
|
||||
#include "test_globals.h"
|
||||
#include "test_init.h"
|
||||
#include "proxysql.h"
|
||||
#include "MySQLErrorClassifier.h"
|
||||
|
||||
// ============================================================================
|
||||
// 1. classify_mysql_error
|
||||
// ============================================================================
|
||||
|
||||
static void test_retryable_errors() {
|
||||
// 1047 (WSREP not ready) with retry conditions met
|
||||
ok(classify_mysql_error(1047, 3, true, false, false) == MYSQL_ERROR_RETRY_ON_NEW_CONN,
|
||||
"1047: retryable when conditions met");
|
||||
// 1053 (server shutdown) with retry conditions met
|
||||
ok(classify_mysql_error(1053, 1, true, false, false) == MYSQL_ERROR_RETRY_ON_NEW_CONN,
|
||||
"1053: retryable when conditions met");
|
||||
}
|
||||
|
||||
static void test_retryable_but_blocked() {
|
||||
// 1047 but no retries left
|
||||
ok(classify_mysql_error(1047, 0, true, false, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"1047: not retried when retries=0");
|
||||
// 1047 but connection not reusable
|
||||
ok(classify_mysql_error(1047, 3, false, false, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"1047: not retried when connection not reusable");
|
||||
// 1047 but in active transaction
|
||||
ok(classify_mysql_error(1047, 3, true, true, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"1047: not retried during active transaction");
|
||||
// 1047 but multiplex disabled
|
||||
ok(classify_mysql_error(1047, 3, true, false, true) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"1047: not retried when multiplex disabled");
|
||||
}
|
||||
|
||||
static void test_non_retryable_errors() {
|
||||
// Common MySQL errors — always report to client
|
||||
ok(classify_mysql_error(1045, 3, true, false, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"1045 (access denied): always report");
|
||||
ok(classify_mysql_error(1064, 3, true, false, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"1064 (syntax error): always report");
|
||||
ok(classify_mysql_error(1146, 3, true, false, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"1146 (table not found): always report");
|
||||
ok(classify_mysql_error(2006, 3, true, false, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"2006 (gone away): always report");
|
||||
ok(classify_mysql_error(0, 3, true, false, false) == MYSQL_ERROR_REPORT_TO_CLIENT,
|
||||
"0 (no error): report");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 2. can_retry_on_new_connection
|
||||
// ============================================================================
|
||||
|
||||
static void test_retry_on_offline() {
|
||||
ok(can_retry_on_new_connection(true, 3, true, false, false, false) == true,
|
||||
"retry: server offline, all conditions met");
|
||||
}
|
||||
|
||||
static void test_no_retry_server_online() {
|
||||
ok(can_retry_on_new_connection(false, 3, true, false, false, false) == false,
|
||||
"no retry: server is online");
|
||||
}
|
||||
|
||||
static void test_no_retry_conditions() {
|
||||
ok(can_retry_on_new_connection(true, 0, true, false, false, false) == false,
|
||||
"no retry: no retries left");
|
||||
ok(can_retry_on_new_connection(true, 3, false, false, false, false) == false,
|
||||
"no retry: connection not reusable");
|
||||
ok(can_retry_on_new_connection(true, 3, true, true, false, false) == false,
|
||||
"no retry: active transaction");
|
||||
ok(can_retry_on_new_connection(true, 3, true, false, true, false) == false,
|
||||
"no retry: multiplex disabled");
|
||||
ok(can_retry_on_new_connection(true, 3, true, false, false, true) == false,
|
||||
"no retry: transfer already started");
|
||||
}
|
||||
|
||||
int main() {
|
||||
plan(19);
|
||||
int rc = test_init_minimal();
|
||||
ok(rc == 0, "test_init_minimal() succeeds");
|
||||
|
||||
test_retryable_errors(); // 2
|
||||
test_retryable_but_blocked(); // 4
|
||||
test_non_retryable_errors(); // 5
|
||||
test_retry_on_offline(); // 1
|
||||
test_no_retry_server_online(); // 1
|
||||
test_no_retry_conditions(); // 5
|
||||
// Total: 1+2+4+5+1+1+5 = 19
|
||||
|
||||
test_cleanup_minimal();
|
||||
return exit_status();
|
||||
}
|
||||
@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @file server_selection_unit-t.cpp
|
||||
* @brief Unit tests for the server selection algorithm.
|
||||
*
|
||||
* Tests the pure selection functions extracted from get_random_MySrvC():
|
||||
* - is_candidate_eligible()
|
||||
* - select_server_from_candidates()
|
||||
*
|
||||
* @see Phase 3.4 (GitHub issue #5492)
|
||||
*/
|
||||
|
||||
#include "tap.h"
|
||||
#include "test_globals.h"
|
||||
#include "test_init.h"
|
||||
|
||||
#include "proxysql.h"
|
||||
#include "ServerSelection.h"
|
||||
|
||||
// ============================================================================
|
||||
// Helper: create a default ONLINE server candidate
|
||||
// ============================================================================
|
||||
static ServerCandidate make_candidate(int idx, int64_t weight = 1,
|
||||
unsigned int max_conns = 1000)
|
||||
{
|
||||
ServerCandidate c {};
|
||||
c.index = idx;
|
||||
c.weight = weight;
|
||||
c.status = SERVER_ONLINE;
|
||||
c.current_connections = 0;
|
||||
c.max_connections = max_conns;
|
||||
c.current_latency_us = 0;
|
||||
c.max_latency_us = 0;
|
||||
c.current_repl_lag = 0;
|
||||
c.max_repl_lag = 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 1. is_candidate_eligible
|
||||
// ============================================================================
|
||||
|
||||
static void test_eligibility() {
|
||||
ServerCandidate online = make_candidate(0);
|
||||
ok(is_candidate_eligible(online) == true, "eligible: ONLINE server");
|
||||
|
||||
ServerCandidate shunned = make_candidate(1);
|
||||
shunned.status = SERVER_SHUNNED;
|
||||
ok(is_candidate_eligible(shunned) == false, "ineligible: SHUNNED");
|
||||
|
||||
ServerCandidate off_soft = make_candidate(2);
|
||||
off_soft.status = SERVER_OFFLINE_SOFT;
|
||||
ok(is_candidate_eligible(off_soft) == false, "ineligible: OFFLINE_SOFT");
|
||||
|
||||
ServerCandidate off_hard = make_candidate(3);
|
||||
off_hard.status = SERVER_OFFLINE_HARD;
|
||||
ok(is_candidate_eligible(off_hard) == false, "ineligible: OFFLINE_HARD");
|
||||
|
||||
ServerCandidate lag_shunned = make_candidate(4);
|
||||
lag_shunned.status = SERVER_SHUNNED_REPLICATION_LAG;
|
||||
ok(is_candidate_eligible(lag_shunned) == false, "ineligible: SHUNNED_REPL_LAG");
|
||||
|
||||
ServerCandidate at_max = make_candidate(5, 1, 10);
|
||||
at_max.current_connections = 10;
|
||||
ok(is_candidate_eligible(at_max) == false, "ineligible: at max_connections");
|
||||
|
||||
ServerCandidate below_max = make_candidate(6, 1, 10);
|
||||
below_max.current_connections = 9;
|
||||
ok(is_candidate_eligible(below_max) == true, "eligible: below max_connections");
|
||||
|
||||
ServerCandidate high_latency = make_candidate(7);
|
||||
high_latency.max_latency_us = 5000;
|
||||
high_latency.current_latency_us = 6000;
|
||||
ok(is_candidate_eligible(high_latency) == false, "ineligible: high latency");
|
||||
|
||||
ServerCandidate ok_latency = make_candidate(8);
|
||||
ok_latency.max_latency_us = 5000;
|
||||
ok_latency.current_latency_us = 4000;
|
||||
ok(is_candidate_eligible(ok_latency) == true, "eligible: acceptable latency");
|
||||
|
||||
ServerCandidate no_limit = make_candidate(9);
|
||||
no_limit.max_latency_us = 0;
|
||||
no_limit.current_latency_us = 999999;
|
||||
ok(is_candidate_eligible(no_limit) == true, "eligible: latency limit disabled (max=0)");
|
||||
|
||||
ServerCandidate high_lag = make_candidate(10);
|
||||
high_lag.max_repl_lag = 10;
|
||||
high_lag.current_repl_lag = 15;
|
||||
ok(is_candidate_eligible(high_lag) == false, "ineligible: high repl lag");
|
||||
|
||||
ServerCandidate ok_lag = make_candidate(11);
|
||||
ok_lag.max_repl_lag = 10;
|
||||
ok_lag.current_repl_lag = 5;
|
||||
ok(is_candidate_eligible(ok_lag) == true, "eligible: acceptable repl lag");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 2. select_server_from_candidates — basic
|
||||
// ============================================================================
|
||||
|
||||
static void test_select_single() {
|
||||
ServerCandidate c = make_candidate(42);
|
||||
int result = select_server_from_candidates(&c, 1, 12345);
|
||||
ok(result == 42, "single server: always selected (idx=42)");
|
||||
}
|
||||
|
||||
static void test_select_empty() {
|
||||
ok(select_server_from_candidates(nullptr, 0, 0) == -1,
|
||||
"empty list: returns -1");
|
||||
}
|
||||
|
||||
static void test_select_all_offline() {
|
||||
ServerCandidate candidates[3];
|
||||
candidates[0] = make_candidate(0); candidates[0].status = SERVER_OFFLINE_HARD;
|
||||
candidates[1] = make_candidate(1); candidates[1].status = SERVER_SHUNNED;
|
||||
candidates[2] = make_candidate(2); candidates[2].status = SERVER_OFFLINE_SOFT;
|
||||
|
||||
ok(select_server_from_candidates(candidates, 3, 999) == -1,
|
||||
"all offline: returns -1");
|
||||
}
|
||||
|
||||
static void test_select_weight_zero() {
|
||||
ServerCandidate c = make_candidate(0, 0);
|
||||
ok(select_server_from_candidates(&c, 1, 12345) == -1,
|
||||
"weight=0: never selected");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 3. Weighted distribution (statistical)
|
||||
// ============================================================================
|
||||
|
||||
static void test_equal_weight_distribution() {
|
||||
ServerCandidate candidates[2];
|
||||
candidates[0] = make_candidate(0, 1);
|
||||
candidates[1] = make_candidate(1, 1);
|
||||
|
||||
int count[2] = {0, 0};
|
||||
const int N = 10000;
|
||||
for (int seed = 0; seed < N; seed++) {
|
||||
int result = select_server_from_candidates(candidates, 2, seed);
|
||||
if (result >= 0 && result <= 1) count[result]++;
|
||||
}
|
||||
|
||||
double pct0 = (double)count[0] / N * 100;
|
||||
ok(pct0 > 30 && pct0 < 70,
|
||||
"equal weight: server 0 selected %.1f%% (expect ~50%%)", pct0);
|
||||
}
|
||||
|
||||
static void test_weighted_distribution() {
|
||||
ServerCandidate candidates[2];
|
||||
candidates[0] = make_candidate(0, 3); // weight 3
|
||||
candidates[1] = make_candidate(1, 1); // weight 1
|
||||
|
||||
int count[2] = {0, 0};
|
||||
const int N = 10000;
|
||||
for (int seed = 0; seed < N; seed++) {
|
||||
int result = select_server_from_candidates(candidates, 2, seed);
|
||||
if (result >= 0 && result <= 1) count[result]++;
|
||||
}
|
||||
|
||||
double pct0 = (double)count[0] / N * 100;
|
||||
ok(pct0 > 60 && pct0 < 90,
|
||||
"3:1 weight: server 0 selected %.1f%% (expect ~75%%)", pct0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 4. Determinism
|
||||
// ============================================================================
|
||||
|
||||
static void test_determinism() {
|
||||
ServerCandidate candidates[3];
|
||||
candidates[0] = make_candidate(0, 2);
|
||||
candidates[1] = make_candidate(1, 3);
|
||||
candidates[2] = make_candidate(2, 5);
|
||||
|
||||
int r1 = select_server_from_candidates(candidates, 3, 42);
|
||||
int r2 = select_server_from_candidates(candidates, 3, 42);
|
||||
ok(r1 == r2, "determinism: same seed → same result");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 5. Mixed eligible/ineligible
|
||||
// ============================================================================
|
||||
|
||||
static void test_mixed_eligibility() {
|
||||
ServerCandidate candidates[4];
|
||||
candidates[0] = make_candidate(0, 1); candidates[0].status = SERVER_SHUNNED;
|
||||
candidates[1] = make_candidate(1, 1); candidates[1].status = SERVER_OFFLINE_HARD;
|
||||
candidates[2] = make_candidate(2, 1); // ONLINE
|
||||
candidates[3] = make_candidate(3, 1); candidates[3].status = SERVER_OFFLINE_SOFT;
|
||||
|
||||
// Only candidate[2] is eligible — must always be selected
|
||||
int pass = 0;
|
||||
for (int seed = 0; seed < 100; seed++) {
|
||||
if (select_server_from_candidates(candidates, 4, seed) == 2) pass++;
|
||||
}
|
||||
ok(pass == 100,
|
||||
"mixed: only eligible server selected 100/100 times");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
int main() {
|
||||
plan(21);
|
||||
|
||||
int rc = test_init_minimal();
|
||||
ok(rc == 0, "test_init_minimal() succeeds");
|
||||
|
||||
test_eligibility(); // 12
|
||||
test_select_single(); // 1
|
||||
test_select_empty(); // 1
|
||||
test_select_all_offline(); // 1
|
||||
test_select_weight_zero(); // 1
|
||||
test_equal_weight_distribution(); // 1
|
||||
test_weighted_distribution(); // 1
|
||||
test_determinism(); // 1
|
||||
test_mixed_eligibility(); // 1
|
||||
// Total: 1+12+1+1+1+1+1+1+1+1 = 21
|
||||
|
||||
test_cleanup_minimal();
|
||||
return exit_status();
|
||||
}
|
||||
Loading…
Reference in new issue