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.
353 lines
11 KiB
353 lines
11 KiB
/**
|
|
* @file reg_test_3273_ssl_con-t.cpp
|
|
* @brief Regression test for SSL busy/infinite loops for frontend connections.
|
|
* @details When client disconnects unexpectedly closing the socket on a SSL connection, depending on the
|
|
* timing conditions, either an infinite loop or a busy loop could take place. These scenarios are:
|
|
* 1. Closed socket while query running on backend (before data arrives), leads to busy loop.
|
|
* 2. Closed socket after all the data has been written into the socket, since no more writing would take
|
|
* place in the socket an infinite loop would take place.
|
|
*/
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <utility>
|
|
#include <vector>
|
|
#include <poll.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <sys/epoll.h>
|
|
|
|
|
|
#include "mysql.h"
|
|
|
|
#include "tap.h"
|
|
#include "command_line.h"
|
|
#include "utils.h"
|
|
|
|
using std::string;
|
|
using std::pair;
|
|
using std::vector;
|
|
|
|
/* Helper function to do the waiting for events on the socket. */
|
|
static int wait_for_mysql(MYSQL *mysql, int status) {
|
|
struct pollfd pfd;
|
|
int timeout, res;
|
|
|
|
pfd.fd = mysql_get_socket(mysql);
|
|
pfd.events =
|
|
(status & MYSQL_WAIT_READ ? POLLIN : 0) |
|
|
(status & MYSQL_WAIT_WRITE ? POLLOUT : 0) |
|
|
(status & MYSQL_WAIT_EXCEPT ? POLLPRI : 0);
|
|
if (status & MYSQL_WAIT_TIMEOUT)
|
|
timeout = 1000*mysql_get_timeout_value(mysql);
|
|
else
|
|
timeout = -1;
|
|
res = poll(&pfd, 1, timeout);
|
|
if (res == 0)
|
|
return MYSQL_WAIT_TIMEOUT;
|
|
else if (res < 0)
|
|
return MYSQL_WAIT_TIMEOUT;
|
|
else {
|
|
int status = 0;
|
|
if (pfd.revents & POLLIN) status |= MYSQL_WAIT_READ;
|
|
if (pfd.revents & POLLOUT) status |= MYSQL_WAIT_WRITE;
|
|
if (pfd.revents & POLLPRI) status |= MYSQL_WAIT_EXCEPT;
|
|
return status;
|
|
}
|
|
}
|
|
|
|
enum BUSY_LOOP_T {
|
|
BUSY_LOOP=0,
|
|
INF_LOOP=1
|
|
};
|
|
|
|
struct th_args__in_t {
|
|
// Input
|
|
int argc { 0 };
|
|
char** argv { nullptr };
|
|
int secs { 0 };
|
|
int busy_loop_type = BUSY_LOOP_T::BUSY_LOOP;
|
|
CommandLine& cl;
|
|
};
|
|
|
|
struct th_args__out_t {
|
|
// Output
|
|
volatile int* query_started { nullptr };
|
|
volatile int* routine_rc { nullptr };
|
|
};
|
|
|
|
struct th_args_t {
|
|
th_args__in_t in_args;
|
|
th_args__out_t out_args {};
|
|
};
|
|
|
|
void* perform_async_query(void* arg) {
|
|
th_args_t* th_args = static_cast<th_args_t*>(arg);
|
|
|
|
MYSQL* mysql = nullptr;
|
|
|
|
{
|
|
CommandLine& cl = th_args->in_args.cl;
|
|
mysql = mysql_init(NULL);
|
|
MYSQL* ret = NULL;
|
|
int query_ret = 0;
|
|
|
|
mysql_options(mysql, MYSQL_OPT_NONBLOCK, 0);
|
|
mysql_ssl_set(mysql, NULL, NULL, NULL, NULL, NULL);
|
|
|
|
int status = 0;
|
|
|
|
if (th_args->in_args.argc == 2 && (strcmp(th_args->in_args.argv[1], "admin") == 0)) {
|
|
status = mysql_real_connect_start(
|
|
&ret, mysql, cl.host, "radmin", "radmin", NULL, 6032, NULL, CLIENT_SSL
|
|
);
|
|
diag("Creating 'Admin' connection thread=%ld ret=%p status=%d", pthread_self(), ret, status);
|
|
} else {
|
|
status = mysql_real_connect_start(
|
|
&ret, mysql, cl.host, cl.username, cl.password, NULL, cl.port, NULL, CLIENT_SSL
|
|
);
|
|
|
|
diag("Creating 'MySQL' connection thread=%ld ret=%p status=%d", pthread_self(), ret, status);
|
|
}
|
|
if (status == 0) {
|
|
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(mysql));
|
|
__sync_fetch_and_add(th_args->out_args.routine_rc, 1);
|
|
return NULL;
|
|
}
|
|
|
|
diag("Continue connection establishment thread=%ld ret=%p status=%d", pthread_self(), ret, status);
|
|
while (status) {
|
|
status = wait_for_mysql(mysql, status);
|
|
status = mysql_real_connect_cont(&ret, mysql, status);
|
|
diag("'mysql_real_connect_cont' thread=%ld ret=%p status=%d", pthread_self(), ret, status);
|
|
}
|
|
|
|
// NOTE: mariadbclient has an incompatibility between SSL and NONBLOCK flags. Flag needs to be reset
|
|
// after 'mysql_real_connect_cont', otherwise API would become blocking.
|
|
mysql_options(mysql, MYSQL_OPT_NONBLOCK, 0);
|
|
int f=fcntl(mysql->net.fd, F_GETFL);
|
|
fcntl(mysql->net.fd, F_SETFL, f|O_NONBLOCK);
|
|
|
|
const string sleep_query { "SELECT SLEEP(" + std::to_string(th_args->in_args.secs) + ")" };
|
|
diag("mysql_query_start thread=%ld query=%s", pthread_self(), sleep_query.c_str());
|
|
|
|
status = mysql_real_query_start(&query_ret, mysql, sleep_query.c_str(), sleep_query.size());
|
|
|
|
if (th_args->in_args.busy_loop_type == BUSY_LOOP_T::INF_LOOP) {
|
|
// NOTE: When signaling after 'mysql_query_start' has finished, ProxySQL wont attempt to write more
|
|
// to the closed pipe, this corresponds to 'busy_loop_type=2' and infinite loop.
|
|
while (status) {
|
|
status = wait_for_mysql(mysql, status);
|
|
status = mysql_real_query_cont(&query_ret, mysql, status);
|
|
diag("'mysql_real_connect_cont' thread=%ld ret=%p status=%d", pthread_self(), ret, status);
|
|
}
|
|
}
|
|
|
|
diag("Signaling query start thread=%ld status=%d query_ret=%d", pthread_self(), status, query_ret);
|
|
__sync_fetch_and_add(th_args->out_args.query_started, 1);
|
|
|
|
// NOTE: Required for triggering the issue, thread exit isn't enough, either 'process exit' or
|
|
// 'close()'. They should be immediate to the previous action, otherwise timing could be invalid.
|
|
close(mysql->net.fd);
|
|
|
|
while (true) {
|
|
diag(
|
|
"Sleeping after query started... thread=%ld status=%d query_ret=%d",
|
|
pthread_self(), status, query_ret
|
|
);
|
|
sleep(1);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
struct pthread_data_t {
|
|
pthread_t id { 0 };
|
|
int query_started { 0 };
|
|
int routine_rc { EXIT_SUCCESS };
|
|
};
|
|
|
|
const int BUSY_THREADS = get_env_int("TAP_SSL_BUSY_WAIT__BUSY_THREADS", 4);
|
|
const int MAX_IDLE_CPU = get_env_int("TAP_SSL_BUSY_WAIT__MAX_IDLE_CPU", 20);
|
|
const int MAX_BUSY_CPU = get_env_int("TAP_SSL_BUSY_WAIT__MAX_BUSY_CPU", 25);
|
|
|
|
// NOTE: '10' is a nice value due to it's relationship with the 'system_cpu' interval
|
|
const int BUSY_WAIT_SECS = get_env_int("TAP_SSL_BUSY_WAIT__BUSY_WAIT_SECS", 10);
|
|
// NOTE: '5' is the min value due to time interval rounding 'round_intv_to_time_interval'
|
|
const int SAMPLE_INTV_SECS = get_env_int("TAP_SSL_BUSY_WAIT__SAMPLE_INTV_SEC", BUSY_WAIT_SECS / 2);
|
|
|
|
void create_busy_loops(int argc, char** argv, CommandLine& cl, BUSY_LOOP_T loop_type) {
|
|
vector<pthread_data_t> ths_data {};
|
|
vector<std::unique_ptr<th_args_t>> ths_args {};
|
|
|
|
ths_data.resize(BUSY_THREADS);
|
|
|
|
for (size_t i = 0; i < BUSY_THREADS; i++) {
|
|
pthread_data_t& th_data = ths_data[i];
|
|
std::unique_ptr<th_args_t> th_args {
|
|
new th_args_t {
|
|
th_args__in_t {
|
|
argc, argv, BUSY_WAIT_SECS, loop_type, cl
|
|
},
|
|
th_args__out_t {
|
|
&th_data.query_started,
|
|
&th_data.routine_rc
|
|
}
|
|
}
|
|
};
|
|
|
|
pthread_create(&th_data.id, NULL, perform_async_query, th_args.get());
|
|
ths_args.push_back(std::move(th_args));
|
|
|
|
diag("Thread created thread=%ld", th_data.id);
|
|
}
|
|
|
|
bool missing_query = true;
|
|
bool query_failed = false;
|
|
|
|
while (missing_query && !query_failed) {
|
|
bool all_query_started = true;
|
|
|
|
for (pthread_data_t& th_data : ths_data) {
|
|
bool query_started = __sync_fetch_and_add(&th_data.query_started, 0);
|
|
diag(
|
|
"Thread data thread=%ld routine_rc=%d query_started=%d",
|
|
th_data.id, th_data.routine_rc, query_started
|
|
);
|
|
|
|
if (th_data.id == 0 && query_started == 1) {
|
|
diag(
|
|
"Thread alreay cancelled thread=%ld routine_rc=%d query_started=%d",
|
|
th_data.id, th_data.routine_rc, query_started
|
|
);
|
|
continue;
|
|
}
|
|
|
|
query_failed = __sync_fetch_and_add(&th_data.routine_rc, 0);
|
|
|
|
if (query_failed) {
|
|
diag(
|
|
"Async query failed; aborting test thread=%ld routine_rc=%d query_started=%d",
|
|
th_data.id, th_data.routine_rc, query_started
|
|
);
|
|
break;
|
|
}
|
|
|
|
all_query_started &= query_started;
|
|
|
|
if (query_started) {
|
|
diag(
|
|
"Async query started, killing thread thread=%ld routine_rc=%d query_started=%d",
|
|
th_data.id, th_data.routine_rc, query_started
|
|
);
|
|
pthread_cancel(th_data.id);
|
|
th_data.id = 0;
|
|
} else {
|
|
diag(
|
|
"Waiting for async query to start... thread=%ld routine_rc=%d query_started=%d",
|
|
th_data.id, th_data.routine_rc, query_started
|
|
);
|
|
}
|
|
}
|
|
|
|
missing_query = !all_query_started;
|
|
usleep(500 * 1000);
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
CommandLine cl;
|
|
|
|
if (cl.getEnv()) {
|
|
diag("Failed to get the required environmental variables.");
|
|
return -1;
|
|
}
|
|
|
|
plan(4);
|
|
|
|
MYSQL* admin = mysql_init(NULL);
|
|
if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) {
|
|
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
pair<int,vector<MYSQL*>> p_err_nodes_conns { disable_core_nodes_scheduler(cl, admin) };
|
|
if (p_err_nodes_conns.first) { return EXIT_FAILURE; }
|
|
vector<MYSQL*>& nodes_conns { p_err_nodes_conns.second };
|
|
|
|
diag("Checking ProxySQL idle CPU usage");
|
|
double idle_cpu = 0;
|
|
int ret_i_cpu = get_proxysql_cpu_usage(cl, idle_cpu, SAMPLE_INTV_SECS);
|
|
if (ret_i_cpu) {
|
|
diag("Getting initial CPU usage failed with error - %d", ret_i_cpu);
|
|
diag("Aborting further testing");
|
|
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
ok(
|
|
idle_cpu < MAX_IDLE_CPU, "Idle CPU usage should be below expected - Exp:%d%%, Act: %lf%%",
|
|
MAX_IDLE_CPU, idle_cpu
|
|
);
|
|
|
|
diag("Trigger BUSY_LOOP regression BUSY_THREADS=%d BUSY_WAIT_SECS=%d", BUSY_THREADS, BUSY_WAIT_SECS);
|
|
create_busy_loops(argc, argv, cl, BUSY_LOOP_T::BUSY_LOOP);
|
|
|
|
diag("Checking ProxySQL final CPU usage for 'BUSY_LOOP'");
|
|
double final_cpu_usage = 0;
|
|
int ret_f_cpu = get_proxysql_cpu_usage(cl, final_cpu_usage, SAMPLE_INTV_SECS);
|
|
|
|
ok(
|
|
final_cpu_usage < MAX_BUSY_CPU,
|
|
"ProxySQL CPU usage should be below expected - Exp: %d%%, Act: %lf%%",
|
|
MAX_BUSY_CPU, final_cpu_usage
|
|
);
|
|
|
|
// Extra wait to ensure cleanup of faulty client conns. See 'BUSY_WAIT_SECS' NOTE in def.
|
|
int BUSY_WAIT_CLEANUP = BUSY_WAIT_SECS < 5 ? 5 : BUSY_WAIT_SECS / 2;
|
|
diag("Sleeping for %d secs for BUSY_LOOP client cleanup", BUSY_WAIT_CLEANUP);
|
|
sleep(BUSY_WAIT_CLEANUP);
|
|
|
|
diag("Checking ProxySQL idle CPU usage");
|
|
ret_i_cpu = get_proxysql_cpu_usage(cl, idle_cpu, SAMPLE_INTV_SECS);
|
|
if (ret_i_cpu) {
|
|
diag("Getting initial CPU usage failed with error - %d", ret_i_cpu);
|
|
diag("Aborting further testing");
|
|
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
ok(
|
|
idle_cpu < MAX_IDLE_CPU, "Idle CPU usage should be below expected - Exp:%d%%, Act: %lf%%",
|
|
MAX_IDLE_CPU, idle_cpu
|
|
);
|
|
|
|
diag("Trigger INF_LOOP regression BUSY_THREADS=%d BUSY_WAIT_SECS=%d", BUSY_THREADS, BUSY_WAIT_SECS);
|
|
create_busy_loops(argc, argv, cl, BUSY_LOOP_T::INF_LOOP);
|
|
|
|
diag("Checking ProxySQL final CPU usage for 'BUSY_LOOP'");
|
|
final_cpu_usage = 0;
|
|
ret_f_cpu = get_proxysql_cpu_usage(cl, final_cpu_usage, SAMPLE_INTV_SECS);
|
|
|
|
ok(
|
|
final_cpu_usage < MAX_BUSY_CPU,
|
|
"ProxySQL CPU usage should be below expected - Exp: %d%%, Act: %lf%%",
|
|
MAX_BUSY_CPU, final_cpu_usage
|
|
);
|
|
|
|
// Recover cluster scheduler
|
|
for (MYSQL* myconn : nodes_conns) {
|
|
MYSQL_QUERY_T(myconn, "LOAD SCHEDULER FROM DISK");
|
|
MYSQL_QUERY_T(myconn, "LOAD SCHEDULER TO RUNTIME");
|
|
|
|
mysql_close(myconn);
|
|
}
|
|
|
|
mysql_close(admin);
|
|
|
|
return exit_status();
|
|
}
|