diff --git a/test/tap/tap/utils.cpp b/test/tap/tap/utils.cpp index 27c7128a6e..c4a26c24ee 100644 --- a/test/tap/tap/utils.cpp +++ b/test/tap/tap/utils.cpp @@ -357,8 +357,7 @@ static size_t write_callback(void *data, size_t size, size_t nmemb, void *userp) } CURLcode perform_simple_post( - const string& endpoint, const string& post_params, - uint64_t& curl_res_code, string& curl_out_err + const string& endpoint, const string& params, uint64_t& curl_res_code, string& curl_res_data ) { CURL *curl; CURLcode res; @@ -368,7 +367,7 @@ CURLcode perform_simple_post( curl = curl_easy_init(); if(curl) { curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_params.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, params.c_str()); struct memory response = { 0 }; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&response); @@ -376,13 +375,10 @@ CURLcode perform_simple_post( res = curl_easy_perform(curl); if(res != CURLE_OK) { - curl_out_err = string { curl_easy_strerror(res) }; + curl_res_data = string { curl_easy_strerror(res) }; } else { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &curl_res_code); - - if (curl_res_code != 200) { - curl_out_err = string(response.data, response.size); - } + curl_res_data = string(response.data, response.size); } free(response.data); @@ -392,23 +388,71 @@ CURLcode perform_simple_post( return res; } -int wait_until_enpoint_ready( - string endpoint, string post_params, uint32_t timeout, uint32_t delay +CURLcode perform_simple_get( + const string& endpoint, uint64_t& curl_res_code, string& curl_res_data ) { + CURL *curl; + CURLcode res; + + curl_global_init(CURL_GLOBAL_ALL); + + curl = curl_easy_init(); + if(curl) { + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + struct memory response = { 0 }; + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&response); + + res = curl_easy_perform(curl); + + if(res != CURLE_OK) { + curl_res_data = string { curl_easy_strerror(res) }; + } else { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &curl_res_code); + curl_res_data = string(response.data, response.size); + } + + free(response.data); + curl_easy_cleanup(curl); + } + + return res; +} + +int wait_post_enpoint_ready(string endpoint, string post_params, uint32_t timeout, uint32_t delay) { double waited = 0; int res = -1; while (waited < timeout) { - string curl_str_err {}; + string curl_resp_err {}; uint64_t curl_res_code = 0; - int curl_err = perform_simple_post(endpoint, post_params, curl_res_code, curl_str_err); + int curl_res = perform_simple_post(endpoint, post_params, curl_res_code, curl_resp_err); - if (curl_err != CURLE_OK) { - diag( - "'curl_err_code': %d, 'curl_err': '%s', waiting for '%d'ms...", - curl_err, curl_str_err.c_str(), delay - ); - waited += static_cast(delay) / 1000; + if (curl_res != CURLE_OK || curl_res_code != 200) { + diag("'curl_res': %d, 'curl_err': '%s', waiting for '%d'ms...", curl_res, curl_resp_err.c_str(), delay); + waited += static_cast(delay); + usleep(delay * 1000); + } else { + res = 0; + break; + } + } + + return res; +} + +int wait_get_enpoint_ready(string endpoint, uint32_t timeout, uint32_t delay) { + double waited = 0; + int res = -1; + + while (waited < timeout) { + string curl_resp_err {}; + uint64_t curl_res_code = 0; + int curl_res = perform_simple_get(endpoint, curl_res_code, curl_resp_err); + + if (curl_res != CURLE_OK || curl_res_code != 200) { + diag("'curl_res': %d, 'curl_err': '%s', waiting for '%d'ms...", curl_res, curl_resp_err.c_str(), delay); + waited += static_cast(delay); usleep(delay * 1000); } else { res = 0; @@ -922,3 +966,94 @@ int wait_for_backend_conns( return EXIT_SUCCESS; } } + +string join_path(const string& p1, const string& p2) { + if (p1.back() == '/') { + return p1 + p2; + } else { + return p1 + '/' + p2; + } +} + +int check_endpoint_exists(MYSQL* admin, const ept_info_t& ept, bool& exists) { + const string select_query { + "SELECT count(*) FROM restapi_routes WHERE uri='" + ept.name + "' AND method='" + ept.method + "'" + }; + MYSQL_QUERY(admin, select_query.c_str()); + MYSQL_RES* myres = mysql_store_result(admin); + MYSQL_ROW myrow = mysql_fetch_row(myres); + bool res = EXIT_FAILURE; + + if (myrow && myrow[0]) { + int entry_num = std::atoi(myrow[0]); + exists = entry_num != 0; + + res = EXIT_SUCCESS; + } else { + diag("Invalid resultset returned from query '%s'", select_query.c_str()); + + res = EXIT_FAILURE; + } + + mysql_free_result(myres); + + return res; +} + +const char t_restapi_insert[] { + "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " + "VALUES (1,%ld,'%s','%s','%s','comm')", +}; + +const string base_address { "http://localhost:6070/sync/" }; + +int configure_endpoints( + MYSQL* admin, + const string& script_base_path, + const vector& epts_info, + const ept_info_t& dummy_ept, + bool prevent_dups +) { + MYSQL_QUERY(admin, "DELETE FROM restapi_routes"); + + vector _epts_info { epts_info }; + _epts_info.push_back(dummy_ept); + + for (const ept_info_t& ept : _epts_info) { + string f_exe_name {}; + string_format(ept.file, f_exe_name, ept.name.c_str()); + + const string script_path { join_path(script_base_path, f_exe_name) }; + string insert_query {}; + string_format( + t_restapi_insert, insert_query, ept.timeout, ept.method.c_str(), ept.name.c_str(), script_path.c_str() + ); + + bool duplicate_entry = false; + if (check_endpoint_exists(admin, ept, duplicate_entry)) { + return EXIT_FAILURE; + } + + if (!(prevent_dups && duplicate_entry)) { + MYSQL_QUERY(admin, insert_query.c_str()); + } else { + diag( + "Warning: Test payload trying to insert invalid duplicated entry - uri: '%s', method: '%s'", + ept.name.c_str(), ept.method.c_str() + ); + exit(EXIT_FAILURE); + } + } + + MYSQL_QUERY(admin, "LOAD RESTAPI TO RUNTIME"); + + const string full_endpoint { join_path(base_address, dummy_ept.name) }; + int endpoint_timeout = wait_post_enpoint_ready(full_endpoint, "{}", 1000, 100); + + if (endpoint_timeout) { + diag("Timeout while trying to reach first valid enpoint"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/test/tap/tap/utils.h b/test/tap/tap/utils.h index 043f79d4a9..4b286459e2 100644 --- a/test/tap/tap/utils.h +++ b/test/tap/tap/utils.h @@ -116,10 +116,12 @@ std::vector extract_mysql_rows(MYSQL_RES* my_res); * @return '0' in case the endpoint became available before the timeout, or * '-1' in case the timeout expired. */ -int wait_until_enpoint_ready( +int wait_post_enpoint_ready( std::string endpoint, std::string post_params, uint32_t timeout, uint32_t delay=100 ); +int wait_get_enpoint_ready(std::string endpoint, uint32_t timeout, uint32_t delay=100); + /** * @brief Perform a simple POST query to the specified endpoint using the supplied * 'post_params'. @@ -129,16 +131,17 @@ int wait_until_enpoint_ready( * @param curl_out_err A uint64_t reference returning the result code of the * query in case it has been performed. In case the query couldn't be * performed, this value is never initialized. - * @param curl_out_err A string reference to collect the error as a string reported + * @param curl_res_err A string reference to collect the error as a string reported * by 'libcurl' in case of failure. * * @return The response code of the query in case of the query. */ CURLcode perform_simple_post( - const std::string& endpoint, const std::string& post_params, - uint64_t& curl_res_code, std::string& curl_out_err + const std::string& endpoint, const std::string& params, uint64_t& curl_res_code, std::string& curl_res_data ); +CURLcode perform_simple_get(const std::string& endpoint, uint64_t& curl_res_code, std::string& curl_res_data); + /** * @brief Generates a random string of the length of the provider 'strSize' * parameter. @@ -363,4 +366,69 @@ int wait_for_backend_conns( */ int get_cur_backend_conns(MYSQL* proxy_admin, const std::string& conn_type, uint32_t& found_conn_num); +/** + * @brief Join two string paths. Appends '/' to the first supplied string if doesn't already finish with one. + * @param p1 First part of the path to be joined. + * @param p2 Second string to append to the first path. + * @return A string holding at least one '/' between the two previously supplied strings. + */ +std::string join_path(const std::string& p1, const std::string& p2); + +/** + * @brief Holds the required info for the definition of a RESTAPI endpoint. + */ +struct ept_info_t { + std::string name; + std::string file; + std::string method; + uint64_t timeout; +}; + +/** + * @brief Represents a RESTAPI endpoint request expected to succeed. + */ +struct honest_req_t { + ept_info_t ept_info; + std::vector params; +}; + +/** + * @brief Holds the test payload information for faulty requests. + */ +struct ept_pl_t { + /* @brief Params to be issued in the request against the endpoint */ + std::string params; + /* @brief Expected code to be returned by CURL */ + uint64_t curl_rc; + /* @brief Expected response output returned by CURL */ + uint64_t script_err; +}; + +/** + * @brief Represents a RESTAPI endpoint request expected to fail. + */ +struct faulty_req_t { + ept_info_t ept_info; + std::vector ept_pls; +}; + +/** + * @brief Configure the supplied endpoints using the provided information + * + * @param admin Opened connection to ProxySQL admin interface. + * @param script_base_path Common base path for the scripts location. + * @param epts_info Information of the endpoints to be configured. + * @param dummy_ept Dummy endpoint used to check when interface is ready. + * @param prevent_dups Prevent duplicates when inserting the provided info. + * + * @return EXIT_SUCCESS in case of success, EXIT_FAILURE otherwise. Errors are logged. + */ +int configure_endpoints( + MYSQL* admin, + const std::string& script_base_path, + const std::vector& epts_info, + const ept_info_t& dummy_ept, + bool prevent_dups = true +); + #endif // #define UTILS_H diff --git a/test/tap/tests/Makefile b/test/tap/tests/Makefile index 5028423d9b..35f9cf690d 100644 --- a/test/tap/tests/Makefile +++ b/test/tap/tests/Makefile @@ -176,3 +176,6 @@ prepare_statement_err3024_libmysql: prepare_statement_err3024-t.cpp $(TAP_LIBDIR prepare_statement_err3024_async: prepare_statement_err3024-t.cpp $(TAP_LIBDIR)/libtap.a $(CXX) -DASYNC_API prepare_statement_err3024-t.cpp $(INCLUDEDIRS) $(LDIRS) $(OPT) $(MYLIBS) -lpthread -ldl -std=c++11 -ltap $(STATIC_LIBS) -o prepare_statement_err3024_async-t -DGITVERSION=\"$(GIT_VERSION)\" + +test_wexecvp_syscall_failures-t: test_wexecvp_syscall_failures-t.cpp $(TAP_LIBDIR)/libtap.a + $(CXX) $^ $(INCLUDEDIRS) $(LDIRS) $(OPT) $(MYLIBS) -std=c++11 -Wl,--wrap=pipe,--wrap=fcntl,--wrap=read,--wrap=poll -lpthread -ldl -ltap $(STATIC_LIBS) -o $@ diff --git a/test/tap/tests/reg_test_3223-restapi_return_codes-t.cpp b/test/tap/tests/reg_test_3223-restapi_return_codes-t.cpp index ae7bfa288e..440e5c64f9 100644 --- a/test/tap/tests/reg_test_3223-restapi_return_codes-t.cpp +++ b/test/tap/tests/reg_test_3223-restapi_return_codes-t.cpp @@ -6,199 +6,374 @@ */ #include +#include #include #include +#include #include #include -#include -#include +#include -#include -#include +#include "curl/curl.h" +#include "json.hpp" +#include "mysql.h" +#include "mysql/mysqld_error.h" +#include "command_line.h" #include "proxysql_utils.h" #include "tap.h" -#include "command_line.h" #include "utils.h" using std::string; +using std::vector; + +using hrc = std::chrono::high_resolution_clock; +using nlohmann::json; + +const string base_address { "http://localhost:6070/sync/" }; + +const vector honest_requests { + { { "valid_output_script", "%s.py", "POST", 1000 }, { "{}" } }, + // Check that 'POST' correctly forwards supplied parameters: + // 1 - Target script performs a check on the supplied parameters and fails in case they are unexpected + { { "valid_params_script", "%s.py", "POST", 1000 }, { "{\"param1\": \"value1\", \"param2\": \"value2\"}" } }, + // On top of the previous check, also check that 'GET' allows: + // 1 - Empty parameters: We internally translate into an empty well-formed JSON '{}' + // 2 - Escaped values: As long as the JSON is correct, the RESTAPI forwarding should be able to handle it + { { "valid_params_script", "%s.py", "GET", 1000 }, { "", "?param1='value1'¶m2='\"value2\"'" } }, + // Checks that RESTAPI behaves correctly with scripts with big outputs + { { "large_output_script", "%s.py", "POST", 5000 }, { "{}" } }, + // Checks that RESTAPI is being able to properly read scripts with partial output flushes + { { "partial_output_flush_script", "%s.py", "POST", 10000 }, { "{}" } }, +}; +const vector invalid_requests { + // Checks that 'POST' fails for: + // 1 - Empty parameters. + // 2 - Invalid JSON input. + // 3 - Valid JSON input but unexpected (checking script correctness itself). + { + { "valid_params_script", "%s.py", "POST", 1000 }, + { + // Empty parameters is considered and invalid JSON input for a POST request: + // - '400' error, 'Invalid Request'. + // - Error code of '0', script was never executed. + { "", 400, 0 }, + // Invalid JSON input should result in: + // - '400' error, 'Invalid Request'. + // - Error code of '0', script was never executed. + { "\"param1\": \"value1\", \"param2\": \"value2}", 400, 0 }, + // Valid JSON input, that fails script check: + // - '424' error, script was the one failing during execution. + // - Error code of '1'. Script exit code for failed input validation. + { "{\"foo\": \"bar\"}", 424, 1 }, + } + }, + // Check invalid output (non valid JSON) for 'POST' and 'GET' + { { "invalid_output_script", "%s.py", "POST", 1000 }, { { "{}", 424, 0 } } }, + { { "invalid_output_script", "%s.py", "GET", 1000 }, { { "", 424, 0 } } }, + // Check timeout script for 'POST' and 'GET' + { { "timeout_script", "%s.py", "POST", 1000 }, { { "{}", 424, ETIME } } }, + { { "timeout_script", "%s.py", "GET", 1000 }, { { "", 424, ETIME } } }, + // Check error code from script failing to execute (Python exit code) + { { "invalid_script", "%s.py", "POST", 1000 }, { { "{}", 424, 1 } } }, + // Check non being able to execute the target script - Invalid Path + { { "non_existing_script", "%s", "POST", 1000 }, { { "{}", 424, ENOENT } } }, + // Check non being able to execute the target script - Insufficient perms + { { "script_no_permissions", "%s", "POST", 1000 }, { { "{}", 424, EACCES } } }, + // Check error code from script killed by signal + { { "bash_sigsev_script", "%s.bash", "POST", 1000 }, { { "{}", 424, 128 + SIGSEGV } } }, + // Check killing of script that closes communication pipes with ProxySQL + { { "close_stdout_script", "%s.py", "POST", 1000 }, { { "{}", 424, ETIME } } }, +}; -const std::string base_address { "http://localhost:6070/sync/" }; +int count_exp_tests(const vector& v1, const vector& v2) { + int exp_tests = 0; -std::vector> valid_endpoints { - std::make_tuple( "large_output_script", "{}", 200 ), - std::make_tuple( "partial_output_flush_script", "{}", 200 ), - std::make_tuple( "valid_output_script", "{}", 200 ), - // check scripts remain operational - // NOTE: Disable due to requiring python2 annotation in file - // std::make_tuple( "metrics", "{\"user\":\"admin\", \"password\":\"admin\", \"host\":\"127.0.0.1\", \"port\":\"6032\"}", 200 ) -}; + for (const honest_req_t& req : v1) { + exp_tests += req.params.size() * 3; + } -std::vector> invalid_requests { - std::make_tuple( "invalid_output_script", "{}", 424 ), - std::make_tuple( "timeout_script", "{}", 424 ), - std::make_tuple( "invalid_script", "{}", 424 ), - std::make_tuple( "non_existing_script", "{}", 400 ), - // supplied with invalid params - std::make_tuple( "valid_output_script", "", 400 ) -}; + for (const faulty_req_t& req : v2) { + for (const ept_pl_t& ept_pl : req.ept_pls) { + if (ept_pl.script_err != ETIME) { + exp_tests += 4; + } else { + exp_tests += 5; + } + } + } + + return exp_tests; +} + +/** + * @details + * ProxySQL could give a total grace period of '4' seconds to a finishing script before issuing a SIGKILL. + * This scenario takes place when something goes wrong during the script execution in ProxySQL side, or when + * the script closes the communication PIPES without finishing. + * - Initial wait of 1s second after communication is broken waiting for script to gracefully finish. + * - A maximum 3s of waiting time after SIGTERM before issuing SIGKILL. + */ +const uint32_t PROXY_GRACE_PERIOD = 1000 + 3000; int main(int argc, char** argv) { CommandLine cl; if (cl.getEnv()) { diag("Failed to get the required environmental variables."); - return -1; + return EXIT_FAILURE; } - plan(valid_endpoints.size() + invalid_requests.size()); + plan(count_exp_tests(honest_requests, invalid_requests)); - MYSQL* proxysql_admin = mysql_init(NULL); + MYSQL* admin = mysql_init(NULL); // Initialize connections - if (!proxysql_admin) { - fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); - return -1; + if (!admin) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); + return EXIT_FAILURE; } - if (!mysql_real_connect(proxysql_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(proxysql_admin)); - return -1; + 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; } // Enable 'RESTAPI' - MYSQL_QUERY(proxysql_admin, "SET admin-restapi_enabled='true'"); - MYSQL_QUERY(proxysql_admin, "SET admin-restapi_port=6070"); + MYSQL_QUERY(admin, "SET admin-restapi_enabled='true'"); + MYSQL_QUERY(admin, "SET admin-restapi_port=6070"); - MYSQL_QUERY(proxysql_admin, "LOAD ADMIN VARIABLES TO RUNTIME"); + MYSQL_QUERY(admin, "LOAD ADMIN VARIABLES TO RUNTIME"); // Clean current 'restapi_routes' if any - MYSQL_QUERY(proxysql_admin, "DELETE FROM restapi_routes"); + MYSQL_QUERY(admin, "DELETE FROM restapi_routes"); // Configure restapi_routes to be used - std::string test_script_base_path { - std::string { cl.workdir } + "reg_test_3223_scripts" - }; - - std::vector t_valid_scripts_inserts { - "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " - "VALUES (1,5000,'POST','large_output_script','%s/large_output_script.py','comm')", - "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " - "VALUES (1,5000,'POST','partial_output_flush_script','%s/partial_output_flush_script.py','comm')", - "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " - "VALUES (1,1000,'POST','valid_output_script','%s/valid_output_script.py','comm')" - }; - std::vector valid_scripts_inserts {}; - - for (const auto& t_valid_script_insert : t_valid_scripts_inserts) { - std::string valid_script_insert {}; - string_format(t_valid_script_insert, valid_script_insert, test_script_base_path.c_str()); - valid_scripts_inserts.push_back(valid_script_insert); - } - - // Configure routes for valid scripts - for (const auto& valid_script_insert : valid_scripts_inserts) { - MYSQL_QUERY( - proxysql_admin, - valid_script_insert.c_str() - ); - } + string script_base_path { string { cl.workdir } + "reg_test_3223_scripts" }; + const ept_info_t dummy_ept { "dummy_ept_script", "%s.py", "POST", 1000 }; + + // Configure the valid requests + vector v_epts_info {}; + const auto ext_v_epts_info = [] (const honest_req_t& req) { return req.ept_info; }; + std::transform( + honest_requests.begin(), honest_requests.end(), std::back_inserter(v_epts_info), ext_v_epts_info + ); - // NOTE: Disable due to requiring python2 annotation in file - // MYSQL_QUERY( - // proxysql_admin, - // "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " - // "VALUES (1,1000,'POST','metrics','../scripts/metrics.py','comm')" - // ); - - std::vector t_invalid_scripts_inserts { - "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " - "VALUES (1,1000,'POST','invalid_output_script','%s/invalid_output_script.py','comm')", - "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " - "VALUES (1,1000,'POST','timeout_script','%s/timeout_script.py','comm')", - "INSERT INTO restapi_routes (active, timeout_ms, method, uri, script, comment) " - "VALUES (1,1000,'POST','invalid_script','%s/invalid_script.py','comm')" - }; - std::vector invalid_scripts_inserts {}; - - for (const auto& t_invalid_script_insert : t_invalid_scripts_inserts) { - std::string invalid_script_insert {}; - string_format(t_invalid_script_insert, invalid_script_insert, test_script_base_path.c_str()); - invalid_scripts_inserts.push_back(invalid_script_insert); + int ept_conf_res = configure_endpoints(admin, script_base_path, v_epts_info, dummy_ept, true); + if (ept_conf_res) { + diag("Endpoint configuration failed. Skipping endpoint testing..."); + return EXIT_FAILURE; } - // Configure routes for invalid scripts - for (const auto& invalid_script_insert : invalid_scripts_inserts) { - MYSQL_QUERY( - proxysql_admin, - invalid_script_insert.c_str() - ); + { + for (const auto& req : honest_requests) { + for (const string& params : req.params) { + const string ept { join_path(base_address, req.ept_info.name) }; + diag( + "%s: Checking valid '%s' request - ept: '%s', params: '%s'", tap_curtime().c_str(), + req.ept_info.method.c_str(), ept.c_str(), params.c_str() + ); + std::chrono::nanoseconds duration; + hrc::time_point start; + hrc::time_point end; + + string curl_res_data { "" }; + uint64_t curl_res_code = 0; + + CURLcode post_err = CURLE_AGAIN; + + if (req.ept_info.method == "POST") { + start = hrc::now(); + post_err = perform_simple_post(ept, params, curl_res_code, curl_res_data); + end = hrc::now(); + + duration = end - start; + double duration_ms = duration.count() / static_cast(1000*1000); + + ok( + duration_ms < (req.ept_info.timeout + PROXY_GRACE_PERIOD), + "Request duration should always be smaller than (timeout + grace period) -" + " timeout: '%ld', duration_ms: '%lf', grace_period: '%d'", + req.ept_info.timeout, duration_ms, PROXY_GRACE_PERIOD + ); + + bool json_parse_failure = false; + + try { + json _ = json::parse(curl_res_data); + } catch (std::exception& e) { + diag("Failed to parse response JSON - '%s'", e.what()); + json_parse_failure = true; + } + + const char* out_res_data = "'skipped_due_to_size'"; + + if (curl_res_data.length() > 50) { + diag("Omit print of 'curl_res_data' due to big size '%ld'", curl_res_data.length()); + } else { + out_res_data = curl_res_data.c_str(); + } + + ok( + json_parse_failure == false, + "Valid JSON received for VALID '%s' request - params: '%s', curl_err_resp: '%s'", + req.ept_info.method.c_str(), params.c_str(), out_res_data + ); + + ok( + post_err == CURLE_OK && curl_res_code == 200, + "'%s' over '%s' should result into a '200' res code:" + " (curl_err: '%d', curl_res_code: '%ld', curl_res_data: '%s')", + req.ept_info.method.c_str(), ept.c_str(), post_err, curl_res_code, out_res_data + ); + } else { + const string get_ept { ept + params }; + start = hrc::now(); + post_err = perform_simple_get(get_ept, curl_res_code, curl_res_data); + end = hrc::now(); + bool json_parse_failure = false; + + duration = end - start; + double duration_ms = duration.count() / static_cast(1000*1000); + + ok( + duration_ms < (req.ept_info.timeout + PROXY_GRACE_PERIOD), + "Request duration should always be smaller than (timeout + grace period) -" + " timeout: '%ld', duration_ms: '%lf', grace_period: '%d'", + req.ept_info.timeout, duration_ms, PROXY_GRACE_PERIOD + ); + + try { + json _ = json::parse(curl_res_data); + } catch (std::exception& e) { + diag("Failed to parse response JSON - '%s'", e.what()); + json_parse_failure = true; + } + + const char* out_res_data = "'skipped_due_to_size'"; + + if (curl_res_data.length() > 50) { + diag("Omit print of 'curl_res_data' due to big size '%ld'", curl_res_data.length()); + } else { + out_res_data = curl_res_data.c_str(); + } + + ok( + json_parse_failure == false, + "Valid JSON received for VALID '%s' request - params: '%s', curl_err_resp: '%s'", + req.ept_info.method.c_str(), params.c_str(), out_res_data + ); + + ok( + post_err == CURLE_OK && curl_res_code == 200, + "'%s' over '%s' should result into a '200' res code:" + " (curl_err: '%d', curl_res_code: '%ld', curl_res_data: '%s')", + req.ept_info.method.c_str(), ept.c_str(), post_err, curl_res_code, out_res_data + ); + } + } + } } - MYSQL_QUERY(proxysql_admin, "LOAD restapi TO RUNTIME"); - - // Sensible wait until the new configured enpoints are ready. - // Use the first enpoint for the check - const auto& first_request_tuple { valid_endpoints.front() }; - const std::string full_endpoint { - base_address + std::get<0>(first_request_tuple) + "/" - }; - int endpoint_timeout = wait_until_enpoint_ready( - full_endpoint, std::get<1>(first_request_tuple), 10, 500 + vector i_epts_info {}; + const auto ext_i_epts_info = [] (const faulty_req_t& req) { return req.ept_info; }; + std::transform( + invalid_requests.begin(), invalid_requests.end(), std::back_inserter(i_epts_info), ext_i_epts_info ); - if (endpoint_timeout) { - diag( - "Timeout while trying to reach first valid enpoint." - " Test failed, skipping endpoint testing..." - ); - goto skip_endpoints_testing; - } - - for (const auto& valid_request_tuple : valid_endpoints) { - const std::string full_endpoint { base_address + std::get<0>(valid_request_tuple) + "/"}; - std::string post_out_err { "" }; - uint64_t curl_res_code = 0; - - // perform the POST operation - CURLcode post_err = perform_simple_post( - full_endpoint, std::get<1>(valid_request_tuple), curl_res_code, post_out_err - ); - - ok( - post_err == CURLE_OK && curl_res_code == 200, - "Performing a POST over endpoint '%s' should result into a 200 response:" - " (curl_err: '%d', response_errcode: '%ld', curlerr: '%s')", - full_endpoint.c_str(), - post_err, - curl_res_code, - post_out_err.c_str() - ); + ept_conf_res = configure_endpoints(admin, script_base_path, i_epts_info, dummy_ept, true); + if (ept_conf_res) { + diag("Endpoint configuration failed. Skipping endpoint testing..."); + return EXIT_FAILURE; } - for (const auto& invalid_request_tuple : invalid_requests) { - const std::string full_endpoint { base_address + std::get<0>(invalid_request_tuple) + "/" }; - std::string post_out_err { "" }; - uint64_t curl_res_code = 0; - - // perform the POST operation - CURLcode post_err = perform_simple_post( - full_endpoint, std::get<1>(invalid_request_tuple), curl_res_code, post_out_err); - - ok( - post_err == CURLE_OK && curl_res_code == std::get<2>(invalid_request_tuple), - "Performing a POST over endpoint '%s' shouldn't result into a 200 response:" - " (curl_err: '%d', response_errcode: '%ld', curlerr: '%s')", - full_endpoint.c_str(), - post_err, - curl_res_code, - post_out_err.c_str() - ); + for (const auto& req : invalid_requests) { + for (const ept_pl_t& ept_pl : req.ept_pls) { + std::chrono::nanoseconds duration; + hrc::time_point start; + hrc::time_point end; + + const string ept { join_path(base_address, req.ept_info.name) }; + diag( + "%s: Checking valid '%s' request - ept: '%s', params: '%s'", tap_curtime().c_str(), + req.ept_info.method.c_str(), ept.c_str(), ept_pl.params.c_str() + ); + + // Get the target script full path + string f_exe_name {}; + string_format(req.ept_info.file, f_exe_name, req.ept_info.name.c_str()); + const string script_path { join_path(script_base_path, f_exe_name) }; + const string sigterm_file_flag { script_path + "-RECV_SIGTERM" }; + + string curl_res_data {}; + uint64_t curl_res_code = 0; + + // Prepare target folder in case of expected ETIME error + if (ept_pl.script_err == ETIME) { + remove(sigterm_file_flag.c_str()); + } + + // Perform the POST operation + CURLcode curl_code = CURLE_AGAIN; + + start = hrc::now(); + if (req.ept_info.method == "POST") { + curl_code = perform_simple_post(ept, ept_pl.params, curl_res_code, curl_res_data); + } else { + const string get_ept { ept + ept_pl.params }; + curl_code = perform_simple_get(get_ept, curl_res_code, curl_res_data); + } + end = hrc::now(); + + duration = end - start; + double duration_ms = duration.count() / static_cast(1000*1000); + + ok( + duration_ms < (req.ept_info.timeout + PROXY_GRACE_PERIOD), + "Request duration should always be smaller than (timeout + grace period) -" + " timeout: '%ld', duration_ms: '%lf', grace_period: '%d'", + req.ept_info.timeout, duration_ms, PROXY_GRACE_PERIOD + ); + + uint64_t script_err = 0; + string str_resp_err {}; + + try { + json curl_err_rsp = json::parse(curl_res_data); + str_resp_err = curl_err_rsp["error"]; + script_err = std::stoi(curl_err_rsp["error_code"].get()); + } catch (std::exception&) {} + + ok( + str_resp_err.empty() == false, + "Valid JSON received for INVALID request - params: '%s', curl_res_data: '%s'", + ept_pl.params.c_str(), curl_res_data.c_str() + ); + + ok( + curl_code == CURLE_OK && ept_pl.curl_rc == curl_res_code, + "'%s' sould have failed: (curl_err: '%d', exp_rc: '%ld', act_rc: '%ld')", + req.ept_info.method.c_str(), curl_code, ept_pl.curl_rc, curl_res_code + ); + + ok( + ept_pl.script_err == script_err, + "Script error code should match expected - Exp: '%ld', Act: '%ld'", + ept_pl.script_err, script_err + ); + + // A SIGTERM signal should have been issued before SIGKILL; script should acknowledge it + if (ept_pl.script_err == ETIME) { + int f_exists = access(sigterm_file_flag.c_str(), F_OK); + ok(f_exists == 0, "Script '%s' should receive a 'SIGTERM' signal", f_exe_name.c_str()); + } + } } skip_endpoints_testing: + mysql_close(admin); return exit_status(); } diff --git a/test/tap/tests/reg_test_3223_scripts/bash_sigsev_script.bash b/test/tap/tests/reg_test_3223_scripts/bash_sigsev_script.bash new file mode 100755 index 0000000000..20da6c1582 --- /dev/null +++ b/test/tap/tests/reg_test_3223_scripts/bash_sigsev_script.bash @@ -0,0 +1,3 @@ +#!/usr/bin/bash + +kill -11 $$ diff --git a/test/tap/tests/reg_test_3223_scripts/close_stdout_script.py b/test/tap/tests/reg_test_3223_scripts/close_stdout_script.py new file mode 100755 index 0000000000..90c549d432 --- /dev/null +++ b/test/tap/tests/reg_test_3223_scripts/close_stdout_script.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +"""Simple script returning a valid empty result for RESTAPI.""" + +import json +import time +import os +import signal +import sys + +def print_inv_json(): + print("invalid_json", file=sys.stdout, flush=False, end="") + +def print_random_dic(): + random_dic = {} + + for i in range(0, 4000000): + random_dic["id_" + str(i)] = "0000000000" + + j_random_dic = json.dumps(random_dic) + print(j_random_dic, file=sys.stderr) + +def close_stdout(): + sys.stdout.close() + os.close(1) + +def close_stderr(): + sys.stdout.close() + os.close(0) + +def flush_stderr(): + sys.stderr.flush() + +def simple_print_and_flush(stdout=True): + if stdout: + print("{}", file=sys.stdout, flush=True, end="") + else: + print("{}", file=sys.stderr, flush=True, end="") + +def signal_handler(signo, stack_frame): + cur_dir = os.path.dirname(os.path.abspath(__file__)) + f_path = os.path.join(cur_dir, f"{__file__}-RECV_SIGTERM") + + fh = open(f_path, 'w') + fh.close() + + time.sleep(20) + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, signal_handler) + simple_print_and_flush(stdout=True) + close_stdout() + close_stderr() + + time.sleep(10) diff --git a/test/tap/tests/reg_test_3223_scripts/dummy_ept_script.py b/test/tap/tests/reg_test_3223_scripts/dummy_ept_script.py new file mode 100755 index 0000000000..19d71ba451 --- /dev/null +++ b/test/tap/tests/reg_test_3223_scripts/dummy_ept_script.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +"""Dummy script to check RESTAPI is ready.""" + +if __name__ == "__main__": + # Return minimal JSON + print('{}') diff --git a/test/tap/tests/reg_test_3223_scripts/partial_output_flush_script.py b/test/tap/tests/reg_test_3223_scripts/partial_output_flush_script.py index fdbf908fc3..24b7eb8ce4 100755 --- a/test/tap/tests/reg_test_3223_scripts/partial_output_flush_script.py +++ b/test/tap/tests/reg_test_3223_scripts/partial_output_flush_script.py @@ -3,7 +3,9 @@ """Simple script to produce a 1MB output flushed in two times to be processed by the RESTAPI.""" import json +import textwrap import time +import sys random_dic = {} @@ -13,11 +15,8 @@ random_dic["id_" + str(i)] = "0000000000" j_random_dic = json.dumps(random_dic) + parts = textwrap.wrap(j_random_dic, len(j_random_dic)//10) - # Split the string in half - firstpart, secondpart = j_random_dic[:len(j_random_dic)//2], j_random_dic[len(j_random_dic)//2:] - - # Partial flush script - print(firstpart, end='', flush=True) - time.sleep(1) - print(secondpart, end='', flush=True) + for part in parts: + print(part, file=sys.stdout, flush=True, end='') + time.sleep(0.5) diff --git a/test/tap/tests/reg_test_3223_scripts/script_no_permissions b/test/tap/tests/reg_test_3223_scripts/script_no_permissions new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/tap/tests/reg_test_3223_scripts/timeout_script.py b/test/tap/tests/reg_test_3223_scripts/timeout_script.py index 829019f293..729e8d0d67 100755 --- a/test/tap/tests/reg_test_3223_scripts/timeout_script.py +++ b/test/tap/tests/reg_test_3223_scripts/timeout_script.py @@ -2,12 +2,26 @@ """Simple script to force a timeout failure in RESTAPI.""" -import time import json +import os +import signal +import time random_dic = {} +def signal_handler(signo, stack_frame): + cur_dir = os.path.dirname(os.path.abspath(__file__)) + f_path = os.path.join(cur_dir, f"{__file__}-RECV_SIGTERM") + + fh = open(f_path, 'w') + fh.close() + + time.sleep(20) + if __name__ == "__main__": + # Register the handler to advice caller of received signal + signal.signal(signal.SIGTERM, signal_handler) + # Forcing the timeout time.sleep(10) diff --git a/test/tap/tests/reg_test_3223_scripts/valid_params_script.py b/test/tap/tests/reg_test_3223_scripts/valid_params_script.py new file mode 100755 index 0000000000..2fc93d936a --- /dev/null +++ b/test/tap/tests/reg_test_3223_scripts/valid_params_script.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +"""Simple script returning its params as result for the RESTAPI.""" + +import json +import sys + +if __name__ == "__main__": + param_num = len(sys.argv) + + if param_num == 2: + exp_args = json.loads(sys.argv[1]) + print(sys.argv[1]) + + if len(exp_args): + arg1 = exp_args["param1"] + arg2 = exp_args["param2"] + + exp_arg1 = arg1 == "value1" or arg1 == "'value1'" + exp_arg2 = arg2 == "value2" or arg2 == "'value2'" or arg2 == "'\"value2\"'" + + sys.exit(not (exp_arg1 and exp_arg2)) + else: + sys.exit(0) + else: + print(json.dumps({"error": f"Invalid number of params - exp: '2', act: '{param_num}'"})) + sys.exit(1) diff --git a/test/tap/tests/reg_test_3223_scripts/write_to_std_streams.py b/test/tap/tests/reg_test_3223_scripts/write_to_std_streams.py new file mode 100755 index 0000000000..e082fdbd98 --- /dev/null +++ b/test/tap/tests/reg_test_3223_scripts/write_to_std_streams.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +"""Simple script that writes a dummy output to the specified standard stream.""" + +import sys +import time +import os +import signal + +def signal_handler(signo, stack_frame): + cur_dir = os.path.dirname(os.path.abspath(__file__)) + f_path = os.path.join(cur_dir, f"{__file__}-RECV_SIGTERM") + + fh = open(f_path, 'w') + fh.close() + + time.sleep(20) + +if __name__ == "__main__": + param_num = len(sys.argv) + + if param_num == 2: + # Register the handler to advice caller of received signal + signal.signal(signal.SIGTERM, signal_handler) + + if sys.argv[1] == "stdout": + print("dummy_stdout_output", file=sys.stdout, flush=True, end='') + elif sys.argv[1] == "stderr": + print("dummy_stderr_output", file=sys.stderr, flush=True, end='') + else: + pass + + # Ensure that we are killed by timeout + time.sleep(20) + else: + sys.exit(1) diff --git a/test/tap/tests/reg_test_3591-restapi_num_fds-t.cpp b/test/tap/tests/reg_test_3591-restapi_num_fds-t.cpp index ca43ce70cf..f859517183 100644 --- a/test/tap/tests/reg_test_3591-restapi_num_fds-t.cpp +++ b/test/tap/tests/reg_test_3591-restapi_num_fds-t.cpp @@ -45,48 +45,50 @@ int main(int argc, char** argv) { setrlimit(RLIMIT_NOFILE, &limits); diag("New process limits: { %ld, %ld }", limits.rlim_cur, limits.rlim_max); - MYSQL* proxysql_admin = mysql_init(NULL); + MYSQL* admin = mysql_init(NULL); // Initialize connections - if (!proxysql_admin) { - fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + if (!admin) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); return -1; } - if (!mysql_real_connect(proxysql_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(proxysql_admin)); + 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 -1; } // Enable 'RESTAPI' - MYSQL_QUERY(proxysql_admin, "SET admin-restapi_enabled='true'"); - MYSQL_QUERY(proxysql_admin, "SET admin-restapi_port=6070"); - MYSQL_QUERY(proxysql_admin, "LOAD ADMIN VARIABLES TO RUNTIME"); + MYSQL_QUERY(admin, "SET admin-restapi_enabled='true'"); + MYSQL_QUERY(admin, "SET admin-restapi_port=6070"); + MYSQL_QUERY(admin, "LOAD ADMIN VARIABLES TO RUNTIME"); std::vector mysql_connections {}; for (int i = 0; i < NUM_CONNECTIONS; i++) { - MYSQL* proxysql_mysql = mysql_init(NULL); + MYSQL* proxy = mysql_init(NULL); if ( !mysql_real_connect( - proxysql_mysql, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0 + proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0 ) ) { fprintf( stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, - mysql_error(proxysql_mysql) + mysql_error(proxy) ); return EXIT_FAILURE; } - mysql_connections.push_back(proxysql_mysql); + mysql_connections.push_back(proxy); } - int endpoint_timeout = wait_until_enpoint_ready("http://localhost:6070/metrics/", "{}", 10, 500); + int endpoint_timeout = wait_get_enpoint_ready("http://localhost:6070/metrics/", 1000, 100); ok(endpoint_timeout == 0, "The endpoint should be available instead of timing out."); for (int i = 0; i < NUM_CONNECTIONS; i++) { mysql_close(mysql_connections[i]); } + mysql_close(admin); + return exit_status(); } diff --git a/test/tap/tests/reg_test_3838-restapi_eintr-t.cpp b/test/tap/tests/reg_test_3838-restapi_eintr-t.cpp index ce1a76bc87..c4d840dbbc 100644 --- a/test/tap/tests/reg_test_3838-restapi_eintr-t.cpp +++ b/test/tap/tests/reg_test_3838-restapi_eintr-t.cpp @@ -42,10 +42,10 @@ using signal_t = int; using rescode_t = long; vector> endpoint_requests { - std::make_tuple("simple_sleep", "1", 200, 18, 0), - std::make_tuple("simple_sleep_timeout", "4", 424, 18, ETIME), - std::make_tuple("simple_sleep_timeout", "4", 424, 19, ETIME), - std::make_tuple("simple_sleep", "2", 424, 15, 15), + std::make_tuple("simple_sleep", "1", 200, SIGCONT, 0), + std::make_tuple("simple_sleep_timeout", "4", 424, SIGCONT, ETIME), + std::make_tuple("simple_sleep_timeout", "4", 424, SIGSTOP, ETIME), + std::make_tuple("simple_sleep", "2", 424, SIGTERM, SIGTERM), }; int main(int argc, char** argv) { @@ -109,7 +109,7 @@ int main(int argc, char** argv) { const string full_endpoint { base_address + std::get<0>(first_request_tuple) + "/" }; - int endpoint_timeout = wait_until_enpoint_ready(full_endpoint, std::get<1>(first_request_tuple), 10, 500); + int endpoint_timeout = wait_post_enpoint_ready(full_endpoint, std::get<1>(first_request_tuple), 10, 500); if (endpoint_timeout) { diag( @@ -165,14 +165,14 @@ int main(int argc, char** argv) { int pid = std::stol(s_pid); int k_res = 0; - if (signal == 18) { + if (signal == SIGCONT) { for (int i = 0; i < SIGNAL_NUM; i++) { - k_res = kill(pid, 19); + k_res = kill(pid, SIGSTOP); if (k_res != 0) { break; } usleep(100*1000); - k_res = kill(pid, 18); + k_res = kill(pid, SIGCONT); if (k_res != 0) { break; } } } else { @@ -193,14 +193,14 @@ int main(int argc, char** argv) { int signaled = 0; int exp_signaled = 0; - if (post_out_err.empty() == false) { - nlohmann::json j_curl_err = nlohmann::json::parse(post_out_err); + nlohmann::json j_curl_err = nlohmann::json::parse(post_out_err); + if (j_curl_err.contains("error_code")) { child_exit_st = std::stol(j_curl_err["error_code"].get()); } // NOTE: This is pointless because the value doesn't change, but it's a demonstration on how to // recover child process exit statuses for debugging purposes. - if (exp_child_exit_st == 15) { + if (exp_child_exit_st == SIGTERM) { exp_signaled = 1; signaled = WIFSIGNALED(child_exit_st); child_exit_st = WTERMSIG(child_exit_st); @@ -217,7 +217,7 @@ int main(int argc, char** argv) { endpoint.c_str(), exp_rc, post_err, curl_res_code, signaled, child_exit_st, post_out_err.c_str() ); } catch (const std::exception& ex) { - diag("Invalid error kind returned by ProxySQL, JSON parsing failed with error: %s", ex.what()); + diag("Invalid error kind returned by ProxySQL, JSON '%s' parsing failed with error: %s", post_out_err.c_str(), ex.what()); } } diff --git a/test/tap/tests/reg_test_3847_admin_lock-t.cpp b/test/tap/tests/reg_test_3847_admin_lock-t.cpp index e3657dd844..e03a1788de 100644 --- a/test/tap/tests/reg_test_3847_admin_lock-t.cpp +++ b/test/tap/tests/reg_test_3847_admin_lock-t.cpp @@ -53,13 +53,13 @@ int main(int argc, char** argv) { int launch_res = -1; std::thread launch_sec_proxy = std::thread([&WORKSPACE,&cl] (int& err_code) -> void { - to_opts wexecvp_opts {}; - wexecvp_opts.select_to_us = 100*1000; - wexecvp_opts.it_delay_us = 500*1000; + to_opts_t wexecvp_opts {}; + wexecvp_opts.poll_to_us = 100*1000; + wexecvp_opts.waitpid_delay_us = 500*1000; // Stop launched process after 20s wexecvp_opts.timeout_us = 20000 * 1000; // Send sigkill 3s after timeout - wexecvp_opts.sigkill_timeout_us = 3000 * 1000; + wexecvp_opts.sigkill_to_us = 3000 * 1000; const string sec_cfg_file = string { cl.workdir } + "reg_test_3847_node_datadir/proxysql_sec.cfg"; const string sec_log_file = string { cl.workdir } + "reg_test_3847_node_datadir/proxysql_sec.log"; @@ -70,7 +70,7 @@ int main(int argc, char** argv) { string s_stdout {}; string s_stderr {}; - int w_res = wexecvp(proxysql_path, proxy_args, &wexecvp_opts, s_stdout, s_stderr); + int w_res = wexecvp(proxysql_path, proxy_args, wexecvp_opts, s_stdout, s_stderr); if (w_res != EXIT_SUCCESS) { diag("'wexecvp' failed with error: %d", w_res); } diff --git a/test/tap/tests/reg_test_4001-restapi_scripts_num_fds-t.cpp b/test/tap/tests/reg_test_4001-restapi_scripts_num_fds-t.cpp new file mode 100644 index 0000000000..e8c162c88e --- /dev/null +++ b/test/tap/tests/reg_test_4001-restapi_scripts_num_fds-t.cpp @@ -0,0 +1,227 @@ +/** + * @file reg_test_4001-restapi_scripts_num_fds-t.cpp + * @brief Regression test for checking that RESTAPI is able to execute scripts when ProxySQL is handling more + * than 'FD_SETSIZE' file descriptors. + * + * @details The tests creates a higher number of connections than the default maximum number of file + * descriptors determined by `FD_SETSIZE` (1024). After doing this, it tries to enable the 'RESTAPI' and + * performs requests to different endpoints while constantly creating and destroying new client connections. + * This covers two scenarios: + * - The usage of the RESTAPI when ProxySQL is using more than `FD_SETSIZE` file descriptors. + * - The reproduction of the scenario leading to crash reported in issue #4001. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "command_line.h" +#include "proxysql_utils.h" +#include "json.hpp" +#include "tap.h" +#include "utils.h" + +using nlohmann::json; +using std::string; +using std::vector; + +const int NUM_CONNECTIONS = 1300; +const string base_address { "http://localhost:6070/sync/" }; + +const vector honest_requests { + { { "valid_output_script", "%s.py", "POST", 1000 }, { "{}" } }, + // Check that 'POST' correctly forwards supplied parameters: + // 1 - Target script performs a check on the supplied parameters and fails in case they are unexpected + { { "valid_params_script", "%s.py", "POST", 1000 }, { "{\"param1\": \"value1\", \"param2\": \"value2\"}" } }, + // On top of the previous check, also check that 'GET' allows: + // 1 - Empty parameters: We internally translate into an empty well-formed JSON '{}' + // 2 - Escaped values: As long as the JSON is correct, the RESTAPI forwarding should be able to handle it + { { "valid_params_script", "%s.py", "GET", 1000 }, { "", "?param1='value1'¶m2='\"value2\"'" } }, +}; + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + diag("Setting new process limits beyond 'FD_SETSIZE'"); + struct rlimit limits { 0, 0 }; + getrlimit(RLIMIT_NOFILE, &limits); + diag("Old process limits: { %ld, %ld }", limits.rlim_cur, limits.rlim_max); + limits.rlim_cur = NUM_CONNECTIONS * 2 + 100; + setrlimit(RLIMIT_NOFILE, &limits); + diag("New process limits: { %ld, %ld }", limits.rlim_cur, limits.rlim_max); + + MYSQL* admin = mysql_init(NULL); + + // Initialize connections + if (!admin) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); + return EXIT_FAILURE; + } + + 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; + } + + string script_base_path { string { cl.workdir } + "reg_test_3223_scripts" }; + const ept_info_t dummy_ept { "dummy_ept_script", "%s.py", "POST", 1000 }; + + vector v_epts_info {}; + const auto ext_v_epts_info = [] (const honest_req_t& req) { return req.ept_info; }; + std::transform( + honest_requests.begin(), honest_requests.end(), std::back_inserter(v_epts_info), ext_v_epts_info + ); + + int ept_conf_res = configure_endpoints(admin, script_base_path, v_epts_info, dummy_ept, true); + if (ept_conf_res) { + diag("Endpoint configuration failed. Skipping endpoint testing..."); + return EXIT_FAILURE; + } + + std::vector mysql_connections {}; + + for (int i = 0; i < NUM_CONNECTIONS; i++) { + MYSQL* proxy = mysql_init(NULL); + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); + return EXIT_FAILURE; + } + mysql_connections.push_back(proxy); + } + + typedef std::chrono::high_resolution_clock hrc; + const uint64_t test_duration = 10000; + + const uint32_t FAILURE_RATE = 10; + int conn_creation_res = EXIT_FAILURE; + uint64_t conn_count = 0; + uint64_t mysql_fails = 0; + + { + std::thread create_conns([&]() -> int { + std::chrono::nanoseconds duration; + hrc::time_point start; + hrc::time_point end; + + start = hrc::now(); + + while (true) { + if (mysql_fails > (conn_count * FAILURE_RATE) / 100) { + diag("Too many mysql failures in connection creation, considering test as failed..."); + conn_creation_res = EXIT_FAILURE; + return EXIT_FAILURE; + } + + MYSQL* proxy = mysql_init(NULL); + + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); + mysql_fails += 1; + } + + int rc = mysql_query(proxy, "DO 1"); + if (rc) { + diag("mysql_errno: '%d', mysql_error: '%s'", mysql_errno(proxy), mysql_error(proxy)); + mysql_fails += 1; + } + + mysql_close(proxy); + end = hrc::now(); + duration = end - start; + + if (duration.count() >= (test_duration*1000*1000)) { + break; + } + + conn_count += 1; + } + + conn_creation_res = EXIT_SUCCESS; + return EXIT_SUCCESS; + }); + + std::chrono::nanoseconds duration; + hrc::time_point start; + hrc::time_point end; + + start = hrc::now(); + + while (true) { + int rc = 0; + + for (const honest_req_t& req : honest_requests) { + for (const string& params : req.params) { + const string ept { join_path(base_address, req.ept_info.name) }; + std::string curl_res_data {}; + uint64_t curl_res_code = 0; + + CURLcode curl_err = CURLE_COULDNT_CONNECT; + + if (req.ept_info.method == "POST") { + curl_err = perform_simple_post(ept, params, curl_res_code, curl_res_data); + + ok( + curl_err == CURLE_OK && curl_res_code == 200, + "'%s' over '%s' should result into a '200' res code: (curl_err: '%d', curl_res_code: '%ld')", + req.ept_info.method.c_str(), ept.c_str(), curl_err, curl_res_code + ); + } else { + const string get_ept { ept + params }; + curl_err = perform_simple_get(get_ept, curl_res_code, curl_res_data); + + ok( + curl_err == CURLE_OK && curl_res_code == 200, + "'%s' over '%s' should result into a '200' res code: (curl_err: '%d', curl_res_code: '%ld')", + req.ept_info.method.c_str(), ept.c_str(), curl_err, curl_res_code + ); + } + + if (curl_err == 7) { + diag("Operation over endpoint failed with 'CURLE_COULDNT_CONNECT'. Aborting..."); + break; + } + } + } + + end = hrc::now(); + duration = end - start; + + if (duration.count() >= test_duration*1000*1000 || rc) { + break; + } + } + + create_conns.join(); + + ok( + conn_creation_res == EXIT_SUCCESS, + "MySQL conns operations shouldn't had more than a '%d'%% failure rate - conn_count: %ld, failures: %ld", + FAILURE_RATE, conn_count, mysql_fails + ); + } +skip_endpoints_testing: + + for (MYSQL* conn : mysql_connections) { + mysql_close(conn); + } + + mysql_close(admin); + + curl_global_cleanup(); + + return exit_status(); +} diff --git a/test/tap/tests/reg_test_stmt_resultset_err_no_rows_php-t.cpp b/test/tap/tests/reg_test_stmt_resultset_err_no_rows_php-t.cpp index 2f1602d219..03a1f0143e 100644 --- a/test/tap/tests/reg_test_stmt_resultset_err_no_rows_php-t.cpp +++ b/test/tap/tests/reg_test_stmt_resultset_err_no_rows_php-t.cpp @@ -32,8 +32,8 @@ int main(int argc, char** argv) { string php_stderr {}; const string php_path { string{ cl.workdir } + "./reg_test_stmt_resultset_err_no_rows.php" }; - to_opts opts {2 * 1000 * 1000, 0, 0, 0}; - int exec_res = wexecvp(php_path, {}, &opts, php_stdout, php_stderr); + to_opts_t opts {2 * 1000 * 1000, 0, 0, 0}; + int exec_res = wexecvp(php_path, {}, opts, php_stdout, php_stderr); diag("Output from executed test: '%s'", php_path.c_str()); diag("========================================================================"); diff --git a/test/tap/tests/test_cluster_sync-t.cpp b/test/tap/tests/test_cluster_sync-t.cpp index 35c504ab69..7764bb6b74 100644 --- a/test/tap/tests/test_cluster_sync-t.cpp +++ b/test/tap/tests/test_cluster_sync-t.cpp @@ -470,7 +470,7 @@ int main(int, char**) { std::string proxy_stdout {}; std::string proxy_stderr {}; int exec_res = wexecvp( - std::string(cl.workdir) + "../../../src/proxysql", { "-f", "-M", "-c", fmt_config_file.c_str() }, NULL, + std::string(cl.workdir) + "../../../src/proxysql", { "-f", "-M", "-c", fmt_config_file.c_str() }, {}, proxy_stdout, proxy_stderr ); diff --git a/test/tap/tests/test_wexecvp_syscall_failures-t.cpp b/test/tap/tests/test_wexecvp_syscall_failures-t.cpp new file mode 100644 index 0000000000..68156b9472 --- /dev/null +++ b/test/tap/tests/test_wexecvp_syscall_failures-t.cpp @@ -0,0 +1,189 @@ +/** + * @file test_wexecvp_syscall_failures.cpp + * @brief Makes use of GCC wrap option for testing 'wexecvp' over different syscall failures. + * @date 2022-12-08 + */ + +#include +#include +#include + +#include +#include + +#include "proxysql_utils.h" +#include "tap.h" +#include "utils.h" + +using std::string; +using std::vector; +using std::pair; + +bool g_read_use_real = false; +int g_read_ret = -1; +int g_read_errno = EINVAL; + +extern "C" ssize_t __real_read(int __fd, void *__buf, size_t __nbytes); +extern "C" ssize_t __wrap_read(int fd, void* buf, size_t nbytes); + +ssize_t __wrap_read(int fd, void* buf, size_t nbytes) { + if (g_read_use_real) { + return __real_read(fd, buf, nbytes); + } + + errno = g_read_errno; + return g_read_ret; +} + +bool g_pipe_use_real = true; +int g_pipe_ret = -1; +int g_pipe_errno = EINVAL; + +extern "C" int __real_pipe(int __pipedes[2]); +extern "C" int __wrap_pipe(int __pipedes[2]); + +int __wrap_pipe(int __pipedes[2]) { + if (g_pipe_use_real) { + return __real_pipe(__pipedes); + } + + errno = g_pipe_errno; + return g_pipe_ret; +} + +bool g_fcntl_use_real = false; +int g_fcntl_ret = -1; +int g_fcntl_errno = EINVAL; + +extern "C" int __real_fcntl(int __fd, int __cmd, ...); +extern "C" int __wrap_fcntl(int __fd, int __cmd, ...); + +int __wrap_fcntl(int __fd, int __cmd, ...) { + if (g_fcntl_use_real) { + va_list args; + va_start(args, __cmd); + int res = __real_fcntl(__fd, __cmd, args); + va_end(args); + + return res; + } + + usleep(500 * 1000); + + errno = g_fcntl_errno; + return g_fcntl_ret; +} + +bool g_poll_use_real = false; +int g_poll_ret = -1; +int g_poll_errno = EINVAL; + +extern "C" int __real_poll(struct pollfd *__fds, nfds_t __nfds, int __timeout); +extern "C" int __wrap_poll(struct pollfd *__fds, nfds_t __nfds, int __timeout); + +int __wrap_poll(struct pollfd *__fds, nfds_t __nfds, int __timeout) { + if (g_poll_use_real) { + return __real_poll(__fds, __nfds, __timeout); + } + + usleep(500 * 1000); + + errno = g_poll_errno; + return g_poll_ret; +} + +using errno_t = int; +using test_pl_t = pair; + +void enable_reals(const vector& test_pls) { + for (const test_pl_t& test_pl : test_pls) { + test_pl.first = true; + } +} + +int main(int argc, char** argv) { + const char* workdir = getenv("TAP_WORKDIR"); + + if (!workdir) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + const string base_path { join_path(workdir, "reg_test_3223_scripts") }; + const auto check_read_failure = + [] (const string& base_path, const test_pl_t& pl, const string& std_stream) -> void { + const string exe_path { join_path(base_path, "write_to_std_streams.py") }; + + string o_stdout {}; + string o_stderr {}; + + to_opts_t wexecvp_opts {}; + wexecvp_opts.timeout_us = 1*1000*1000; + wexecvp_opts.waitpid_delay_us = 100*1000; + wexecvp_opts.sigkill_to_us = 500*1000; + + const string sigterm_file_flag { exe_path + "-RECV_SIGTERM" }; + diag("Removing previous SIGTERM flag file - '%s'", sigterm_file_flag.c_str()); + remove(sigterm_file_flag.c_str()); + + diag("Launching executable '%s' with params '%s'", exe_path.c_str(), std_stream.c_str()); + int err = wexecvp(exe_path, { std_stream.c_str() }, wexecvp_opts, o_stdout, o_stderr); + + int exp_err = 0; + int exp_errno = 0; + int _errno = errno; + + // From function spec itself, 'read' failure implies '-6' + if ((std_stream == "stdout" || std_stream == "stderr" ) || pl.second != -5) { + exp_err = pl.second; + exp_errno = EINVAL; + } else { + // If no output is performed by the target script, no 'read' should be call by ProxySQL. No read + // calls should imply no failure. + exp_err = ETIME; + exp_errno = 0; + _errno = 0; + } + + ok( + exp_err == err && _errno == exp_errno, + "'wexecvp' should return exp err - { exp_err: %d, act_err:%d }, { exp_errno: %d, act_errno: %d }", + exp_err, err, _errno, exp_errno + ); + + if (pl.second <= -3) { + int f_exists = access(sigterm_file_flag.c_str(), F_OK); + ok(f_exists == 0, "Script '%s' should receive a 'SIGTERM' signal", exe_path.c_str()); + } + }; + + const vector test_pls { + { g_pipe_use_real, -1 }, + { g_fcntl_use_real, -3 }, + { g_poll_use_real, -4 }, + { g_read_use_real, -5 } + }; + + uint32_t planned_tests = 0; + for (const test_pl_t& pl : test_pls) { + if (pl.second <= -3) { + planned_tests += 3 * 2; + } else { + planned_tests += 3; + } + } + + plan(planned_tests); + + for (const test_pl_t& pl : test_pls) { + enable_reals(test_pls); + + pl.first = false; + + check_read_failure(base_path, pl, "stdout"); + check_read_failure(base_path, pl, "stderr"); + check_read_failure(base_path, pl, ""); + } + + return exit_status(); +} diff --git a/test/tap/tests_with_deps/deprecate_eof_support/deprecate_eof_cache-t.cpp b/test/tap/tests_with_deps/deprecate_eof_support/deprecate_eof_cache-t.cpp index 7ca331d428..23aa10eec9 100644 --- a/test/tap/tests_with_deps/deprecate_eof_support/deprecate_eof_cache-t.cpp +++ b/test/tap/tests_with_deps/deprecate_eof_support/deprecate_eof_cache-t.cpp @@ -148,14 +148,14 @@ int main(int argc, char** argv) { std::string select_query {}; string_format(t_select_query, select_query, id); - to_opts opts { 10000*1000, 100*1000, 500*1000, 2000*1000 }; + to_opts_t opts { 10000*1000, 100*1000, 500*1000, 2000*1000 }; // Query *without* support for EOF deprecation auto eof_query = [&] (std::string& query_res, std::string& eof_query_err) -> int { int exec_res = wexecvp( std::string(cl.workdir) + "fwd_eof_query", { select_query.c_str() }, - &opts, + opts, query_res, eof_query_err ); @@ -168,7 +168,7 @@ int main(int argc, char** argv) { int exec_res = wexecvp( std::string(cl.workdir) + "fwd_eof_ok_query", { select_query.c_str() }, - &opts, + opts, query_res, ok_query_err );