diff --git a/test/tap/tests/test_default_value_transaction_isolation_attr-t.cpp b/test/tap/tests/test_default_value_transaction_isolation_attr-t.cpp new file mode 100644 index 000000000..dec96b820 --- /dev/null +++ b/test/tap/tests/test_default_value_transaction_isolation_attr-t.cpp @@ -0,0 +1,460 @@ +/** + * @file test_default_value_transaction_isolation_attr-t.cpp + * @brief This test is meant to test feature introduced in #3466. + * @details The tests performs the following actions to verify that the feature + * is behaving properly: + * - Creates several new users with different values for the introduced + * attribute 'default-transaction_isolation'. + * - Connects with each of the create users verifying that the value has + * been correctly tracked for the frontend connection. + * - Performs the query 'SELECT @@transaction_isolation' to verify that + * the value is correctly propagated to the backend connection. + * - Explicitly sets the value and checks that ProxySQL is properly + * tracking it performing again the two previous actions. + * + * @date 2021-06-14 + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "tap.h" +#include "command_line.h" +#include "proxysql_utils.h" +#include "utils.h" +#include "json.hpp" + +using std::string; +using namespace nlohmann; + +/** + * @brief Helper function to convert a 'MYSQL_RES' into a + * nlohmann::json. + * + * @param result The 'MYSQL_RES*' to be converted into JSON. + * @param j 'nlohmann::json' output parameter holding the + * converted 'MYSQL_RES' supplied. + */ +void parse_result_json_column(MYSQL_RES *result, json& j) { + if(!result) return; + MYSQL_ROW row; + + while ((row = mysql_fetch_row(result))) { + j = json::parse(row[0]); + } +} + +/** + * @brief Create a MySQL user for testing purposes in the server determined + * by supplied *already established* MySQL connection. + * + * @param mysql_server An already opened connection to a MySQL server. + * @param user The name of the user to be created. + * @param pass The password for the user to be created. + * + * @return EXIT_SUCCESS in case of success, EXIT_FAILURE otherwise. + */ +int create_mysql_user( + MYSQL* mysql_server, + const std::string& user, + const std::string& pass +) { + const std::string t_drop_user_query { "DROP USER IF EXISTS %s@'%%'" }; + std::string drop_user_query {}; + string_format(t_drop_user_query, drop_user_query, user.c_str()); + + const std::string t_create_user_query { + "CREATE USER IF NOT EXISTS %s@'%%' IDENTIFIED WITH 'mysql_native_password' BY \"%s\"" + }; + std::string create_user_query {}; + string_format(t_create_user_query, create_user_query, user.c_str(), pass.c_str()); + + const std::string t_grant_usage_query { "GRANT USAGE ON *.* TO %s@'%%'" }; + std::string grant_usage_query { }; + string_format(t_grant_usage_query, grant_usage_query, user.c_str()); + + MYSQL_QUERY(mysql_server, drop_user_query.c_str()); + MYSQL_QUERY(mysql_server, create_user_query.c_str()); + MYSQL_QUERY(mysql_server, grant_usage_query.c_str()); + + return EXIT_SUCCESS; +} + +/** + * @brief Creates the new supplied user in ProxySQL with the provided + * attributes. + * + * @param proxysql_admin An already opened connection to ProxySQL Admin. + * @param user The username of the user to be created. + * @param pass The password of the user to be created. + * @param attributes The 'attributes' value for the 'attributes' column + * for the user to be created. + * + * @return EXIT_SUCCESS in case of success, EXIT_FAILURE otherwise. + */ +int create_proxysql_user( + MYSQL* proxysql_admin, + const std::string& user, + const std::string& pass, + const std::string& attributes +) { + std::string t_del_user_query { "DELETE FROM mysql_users WHERE username='%s'" }; + std::string del_user_query {}; + string_format(t_del_user_query, del_user_query, user.c_str()); + + std::string t_insert_user { + "INSERT INTO mysql_users (username,password,active,attributes)" + " VALUES ('%s','%s',1,'%s')" + }; + std::string insert_user {}; + string_format(t_insert_user, insert_user, user.c_str(), pass.c_str(), attributes.c_str()); + + MYSQL_QUERY(proxysql_admin, del_user_query.c_str()); + MYSQL_QUERY(proxysql_admin, insert_user.c_str()); + + return EXIT_SUCCESS; +} + +using user_config = std::tuple; + +/** + * @brief Create the extra required users for the test in + * both MYSQL and ProxySQL. + * + * @param proxysql_admin An already opened connection to ProxySQL admin + * interface. + * @param mysql_server An already opened connection to a backend MySQL + * server. + * @param user_attributes The user attributes whose should be part of user + * configuration in ProxySQL side. + * + * @return EXIT_SUCCESS in case of success, EXIT_FAILURE otherwise. + */ +int create_extra_users( + MYSQL* proxysql_admin, + MYSQL* mysql_server, + const std::vector& users_config +) { + std::vector> v_user_pass {}; + std::transform( + std::begin(users_config), + std::end(users_config), + std::back_inserter(v_user_pass), + [](const user_config& u_config) { + return std::pair { + std::get<0>(u_config), + std::get<1>(u_config) + }; + } + ); + + // create the MySQL users + for (const auto& user_pass : v_user_pass) { + int c_user_res = + create_mysql_user(mysql_server, user_pass.first, user_pass.second); + if (c_user_res) { + return c_user_res; + } + } + + // create the ProxySQL users + for (const auto& user_config : users_config) { + int c_p_user_res = + create_proxysql_user( + proxysql_admin, + std::get<0>(user_config), + std::get<1>(user_config), + std::get<2>(user_config) + ); + if (c_p_user_res) { + return c_p_user_res; + } + } + + return EXIT_SUCCESS; +} + +/** + * @brief User names and attributes to be check and verified. + */ +const std::vector c_user_attributes { + std::make_tuple( + "sbtest1", + "sbtest1", + "{\"default-transaction_isolation\":\"READ COMMITTED\"}", + "SERIALIZABLE" + ), + std::make_tuple( + "sbtest2", + "sbtest2", + "{\"default-transaction_isolation\":\"REPEATABLE READ\"}", + "READ UNCOMMITTED" + ), + std::make_tuple( + "sbtest3", + "sbtest3", + "{\"default-transaction_isolation\":\"READ UNCOMMITTED\"}", + "REPEATABLE READ" + ), + std::make_tuple( + "sbtest4", + "sbtest4", + "{\"default-transaction_isolation\":\"SERIALIZABLE\"}", + "READ UNCOMMITTED" + ) +}; + +int check_front_conn_isolation_level( + MYSQL* proxysql_mysql, + const std::string& exp_iso_level, + const bool set_via_attr +) { + MYSQL_QUERY(proxysql_mysql, "PROXYSQL INTERNAL SESSION"); + json j_status {}; + MYSQL_RES* int_session_res = mysql_store_result(proxysql_mysql); + parse_result_json_column(int_session_res, j_status); + mysql_free_result(int_session_res); + + try { + std::string front_conn_isolation_level = + j_status.at("conn").at("isolation_level"); + + // Set the 'ok_message' depending on whether the value for + // isolation level have been set through an attribute, + // or explicitly via a 'SET statement' + std::string ok_msg {}; + if (set_via_attr) { + ok_msg = std::string { + "Tracked isolation level for frontend connection should match" + " the one specified in the supplied 'user_attribute':\n" + " - (Exp: '%s') == (Act: '%s')", + }; + } else { + ok_msg = std::string { + "Tracked isolation level for frontend connection should match" + " the one explicitly set via 'SET SESSION TRANSACTION ISOLATION LEVEL':\n" + " - (Exp: '%s') == (Act: '%s')", + }; + } + + ok( + front_conn_isolation_level == exp_iso_level, + ok_msg.c_str(), + exp_iso_level.c_str(), + front_conn_isolation_level.c_str() + ); + } catch (std::exception& e) { + diag("Test failed with exception: '%s'", e.what()); + + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +int check_backend_conn_isolation_level( + MYSQL* proxysql_mysql, + const std::string& exp_iso_level, + const bool set_via_attr +) { + // Get 'transaction_isolation' from backend connection + std::string select_trx_iso_query { "SELECT @@transaction_isolation" }; + MYSQL_QUERY(proxysql_mysql, select_trx_iso_query.c_str()); + MYSQL_RES* trx_iso_res = mysql_store_result(proxysql_mysql); + MYSQL_ROW trx_iso_row = mysql_fetch_row(trx_iso_res); + std::string trx_iso_val {}; + + // Verify that the query produced a correct result + if (trx_iso_row && trx_iso_row[0]) { + trx_iso_val = std::string { trx_iso_row[0] }; + } else { + const std::string err_msg { + "Empty result received from query '" + select_trx_iso_query + "'" + }; + fprintf( + stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, + err_msg.c_str() + ); + return EXIT_FAILURE; + } + + // Filter dashes from the query result + std::replace(std::begin(trx_iso_val), std::end(trx_iso_val), '-', ' '); + + // Perform the check over the expected and actual value + std::string t_ok_msg {}; + + if (set_via_attr) { + t_ok_msg = std::string { + "Result of query '" + select_trx_iso_query + "' should match" + " the isolation level supplied in 'user_attribute':\n" + " - (Exp: '%s') == (Act: '%s')", + }; + } else { + t_ok_msg = std::string { + "Result of query '" + select_trx_iso_query + "' should match" + " the isolation level explicitly set via 'SET SESSION TRANSACTION" + " ISOLATION LEVEL':\n" + " - (Exp: '%s') == (Act: '%s')", + }; + } + + ok( + trx_iso_val == exp_iso_level, t_ok_msg.c_str(), + exp_iso_level.c_str(), trx_iso_val.c_str() + ); + + return EXIT_SUCCESS; +} + +int extract_exp_iso_level( + const std::string& user_attribute, + std::string& exp_iso_level +) { + try { + exp_iso_level = + nlohmann::json::parse(user_attribute) + .at("default-transaction_isolation"); + } catch (const std::exception& ex) { + std::string t_err_msg { + "Invalid format supplied in 'user-attribute'. Generated" + " exception was: '%s'" + }; + std::string err_msg {}; + string_format(t_err_msg, err_msg, ex.what()); + + // Log the error while parsing the supplied attribute + diag("%s", err_msg.c_str()); + + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + plan(c_user_attributes.size() * 4); + + MYSQL* proxysql_admin = mysql_init(NULL); + MYSQL* mysql_server = mysql_init(NULL); + + // Creating the new connections + 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 EXIT_FAILURE; + } + if ( + !mysql_real_connect( + mysql_server, cl.host, "root", "root", NULL, 13306, NULL, 0 + ) + ) { + fprintf( + stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, + mysql_error(mysql_server) + ); + return EXIT_FAILURE; + } + + // Creating the new required users + int c_users_res = + create_extra_users(proxysql_admin, mysql_server, c_user_attributes); + if (c_users_res) { return c_users_res; } + + // Load ProxySQL users to runtime + MYSQL_QUERY(proxysql_admin, "LOAD MYSQL USERS TO RUNTIME"); + + // Performing the connection checks + std::vector user_attributes { c_user_attributes }; + auto rng = std::default_random_engine {}; + std::shuffle(std::begin(user_attributes), std::end(user_attributes), rng); + + for (const auto& user_attribute : user_attributes) { + // Create the new connection to verify + MYSQL* proxysql_mysql = mysql_init(NULL); + if ( + !mysql_real_connect( + proxysql_mysql, + cl.host, + std::get<0>(user_attribute).c_str(), + std::get<1>(user_attribute).c_str(), + NULL, cl.port, NULL, 0) + ) { + fprintf( + stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, + mysql_error(proxysql_mysql) + ); + return EXIT_FAILURE; + } + + std::string exp_iso_level {}; + if (extract_exp_iso_level(std::get<2>(user_attribute), exp_iso_level)) { + if (tests_failed()) { return exit_status(); } + else { return EXIT_FAILURE; } + } + + // Verify that the fronted connection is properly tracking the + // isolation level set in the attributes. + if (check_front_conn_isolation_level(proxysql_mysql, exp_iso_level, true)) { + if (tests_failed()) { return exit_status(); } + else { return EXIT_FAILURE; } + } + + // Verify that the backend connection is properly tracking the + // isolation level set in the attributes. + if (check_backend_conn_isolation_level(proxysql_mysql, exp_iso_level, true)) { + if (tests_failed()) { return exit_status(); } + else { return EXIT_FAILURE; } + } + + // Explicitly change the value for 'transaction_isolation' and + // verify it changed. + std::string t_set_trx_iso_level_query { + "SET SESSION TRANSACTION ISOLATION LEVEL %s" + }; + std::string set_trx_iso_level_query {}; + std::string new_exp_iso_level { std::get<3>(user_attribute) }; + string_format( + t_set_trx_iso_level_query, + set_trx_iso_level_query, + new_exp_iso_level.c_str() + ); + MYSQL_QUERY(proxysql_mysql, set_trx_iso_level_query.c_str()); + + // Check again that the expected isolation level have changed for both connections + if (check_front_conn_isolation_level(proxysql_mysql, new_exp_iso_level, false)) { + if (tests_failed()) { return exit_status(); } + else { return EXIT_FAILURE; } + } + + if (check_backend_conn_isolation_level(proxysql_mysql, new_exp_iso_level, false)) { + if (tests_failed()) { return exit_status(); } + else { return EXIT_FAILURE; } + } + + // Close the connection + mysql_close(proxysql_mysql); + } + + return exit_status(); +}