diff --git a/include/MySQL_Thread.h b/include/MySQL_Thread.h index b3b420433..9f53bce2e 100644 --- a/include/MySQL_Thread.h +++ b/include/MySQL_Thread.h @@ -573,6 +573,7 @@ class MySQL_Threads_Handler char * ssl_p2s_crl; char * ssl_p2s_crlpath; int query_cache_size_MB; + int query_cache_soft_ttl_pct; int min_num_servers_lantency_awareness; int aurora_max_lag_ms_only_read_from_replicas; bool stats_time_backend_query; diff --git a/include/proxysql_structs.h b/include/proxysql_structs.h index 289f2c9a5..1a01752c2 100644 --- a/include/proxysql_structs.h +++ b/include/proxysql_structs.h @@ -856,6 +856,7 @@ __thread int mysql_thread___client_host_error_counts; /* variables used for Query Cache */ __thread int mysql_thread___query_cache_size_MB; +__thread int mysql_thread___query_cache_soft_ttl_pct; /* variables used for SSL , from proxy to server (p2s) */ __thread char * mysql_thread___ssl_p2s_ca; @@ -1021,6 +1022,7 @@ extern __thread int mysql_thread___client_host_error_counts; /* variables used for Query Cache */ extern __thread int mysql_thread___query_cache_size_MB; +extern __thread int mysql_thread___query_cache_soft_ttl_pct; /* variables used for SSL , from proxy to server (p2s) */ extern __thread char * mysql_thread___ssl_p2s_ca; diff --git a/include/query_cache.hpp b/include/query_cache.hpp index 1cbf3324f..cf9bbf6ad 100644 --- a/include/query_cache.hpp +++ b/include/query_cache.hpp @@ -30,6 +30,7 @@ struct __QC_entry_t { unsigned long long create_ms; // when the entry was created, monotonic, millisecond granularity unsigned long long expire_ms; // when the entry will expire, monotonic , millisecond granularity unsigned long long access_ms; // when the entry was read last , monotonic , millisecond granularity + bool refreshing; // true when a client will hit the backend to refresh the entry uint32_t column_eof_pkt_offset = 0; uint32_t row_eof_pkt_offset = 0; uint32_t ok_pkt_offset = 0; @@ -89,7 +90,6 @@ class Query_Cache { ~Query_Cache(); void print_version(); bool set(uint64_t user_hash, const unsigned char *kp, uint32_t kl, unsigned char *vp, uint32_t vl, unsigned long long create_ms, unsigned long long curtime_ms, unsigned long long expire_ms, bool deprecate_eof_active); - bool set(uint64_t , const unsigned char *, uint32_t, unsigned char *, uint32_t, unsigned long long, unsigned long long, unsigned long long); unsigned char * get(uint64_t , const unsigned char *, const uint32_t, uint32_t *, unsigned long long, unsigned long long, bool deprecate_eof_active); uint64_t flush(); SQLite3_result * SQL3_getStats(); diff --git a/lib/MySQL_Thread.cpp b/lib/MySQL_Thread.cpp index 1e2ee196d..da1c0cb8f 100644 --- a/lib/MySQL_Thread.cpp +++ b/lib/MySQL_Thread.cpp @@ -529,6 +529,7 @@ static char * mysql_thread_variables_names[]= { (char *)"auto_increment_delay_multiplex_timeout_ms", (char *)"long_query_time", (char *)"query_cache_size_MB", + (char *)"query_cache_soft_ttl_pct", (char *)"ping_interval_server_msec", (char *)"ping_timeout_server", (char *)"default_schema", @@ -1139,6 +1140,7 @@ MySQL_Threads_Handler::MySQL_Threads_Handler() { variables.auto_increment_delay_multiplex_timeout_ms=10000; variables.long_query_time=1000; variables.query_cache_size_MB=256; + variables.query_cache_soft_ttl_pct=0; variables.init_connect=NULL; variables.ldap_user_variable=NULL; variables.add_ldap_user_comment=NULL; @@ -2252,6 +2254,7 @@ char ** MySQL_Threads_Handler::get_variables_list() { VariablesPointers_int["max_transaction_idle_time"] = make_tuple(&variables.max_transaction_idle_time, 1000, 20*24*3600*1000, false); VariablesPointers_int["max_transaction_time"] = make_tuple(&variables.max_transaction_time, 1000, 20*24*3600*1000, false); VariablesPointers_int["query_cache_size_mb"] = make_tuple(&variables.query_cache_size_MB, 0, 1024*10240, false); + VariablesPointers_int["query_cache_soft_ttl_pct"] = make_tuple(&variables.query_cache_soft_ttl_pct, 0, 100, false); #ifdef IDLE_THREADS VariablesPointers_int["session_idle_ms"] = make_tuple(&variables.session_idle_ms, 1, 3600*1000, false); #endif // IDLE_THREADS @@ -3986,6 +3989,7 @@ void MySQL_Thread::refresh_variables() { mysql_thread___default_max_latency_ms=GloMTH->get_variable_int((char *)"default_max_latency_ms"); mysql_thread___long_query_time=GloMTH->get_variable_int((char *)"long_query_time"); mysql_thread___query_cache_size_MB=GloMTH->get_variable_int((char *)"query_cache_size_MB"); + mysql_thread___query_cache_soft_ttl_pct=GloMTH->get_variable_int((char *)"query_cache_soft_ttl_pct"); mysql_thread___ping_interval_server_msec=GloMTH->get_variable_int((char *)"ping_interval_server_msec"); mysql_thread___ping_timeout_server=GloMTH->get_variable_int((char *)"ping_timeout_server"); mysql_thread___shun_on_failures=GloMTH->get_variable_int((char *)"shun_on_failures"); diff --git a/lib/Query_Cache.cpp b/lib/Query_Cache.cpp index a6af25a47..6ff147695 100644 --- a/lib/Query_Cache.cpp +++ b/lib/Query_Cache.cpp @@ -653,22 +653,35 @@ unsigned char * Query_Cache::get(uint64_t user_hash, const unsigned char *kp, co if (entry!=NULL) { unsigned long long t=curtime_ms; if (entry->expire_ms > t && entry->create_ms + cache_ttl > t) { - THR_UPDATE_CNT(__thr_cntGetOK,Glo_cntGetOK,1,1); - THR_UPDATE_CNT(__thr_dataOUT,Glo_dataOUT,entry->length,1); - - if (deprecate_eof_active && entry->column_eof_pkt_offset) { - result = eof_to_ok_packet(entry); - *lv = entry->length + eof_to_ok_dif; - } else if (!deprecate_eof_active && entry->ok_pkt_offset){ - result = ok_to_eof_packet(entry); - *lv = entry->length + ok_to_eof_dif; + if ( + mysql_thread___query_cache_soft_ttl_pct && !entry->refreshing && + entry->create_ms + cache_ttl * mysql_thread___query_cache_soft_ttl_pct / 100 <= t + ) { + // If the Query Cache entry reach the soft_ttl but do not reach + // the cache_ttl, the next query hit the backend and refresh + // the entry, including ResultSet and TTLs. While the + // refreshing is in process, other queries keep using the "old" + // Query Cache entry. + // soft_ttl_pct with value 0 and 100 disables the functionality. + entry->refreshing = true; } else { - result = (unsigned char *)malloc(entry->length); - memcpy(result, entry->value, entry->length); - *lv = entry->length; - } + THR_UPDATE_CNT(__thr_cntGetOK,Glo_cntGetOK,1,1); + THR_UPDATE_CNT(__thr_dataOUT,Glo_dataOUT,entry->length,1); + + if (deprecate_eof_active && entry->column_eof_pkt_offset) { + result = eof_to_ok_packet(entry); + *lv = entry->length + eof_to_ok_dif; + } else if (!deprecate_eof_active && entry->ok_pkt_offset){ + result = ok_to_eof_packet(entry); + *lv = entry->length + ok_to_eof_dif; + } else { + result = (unsigned char *)malloc(entry->length); + memcpy(result, entry->value, entry->length); + *lv = entry->length; + } - if (t > entry->access_ms) entry->access_ms=t; + if (t > entry->access_ms) entry->access_ms=t; + } } __sync_fetch_and_sub(&entry->ref_count,1); } @@ -683,6 +696,7 @@ bool Query_Cache::set(uint64_t user_hash, const unsigned char *kp, uint32_t kl, entry->column_eof_pkt_offset=0; entry->row_eof_pkt_offset=0; entry->ok_pkt_offset=0; + entry->refreshing=false; // Find the first EOF location unsigned char* it = vp; diff --git a/test/tap/tests/test_query_cache_soft_ttl_pct-t.cpp b/test/tap/tests/test_query_cache_soft_ttl_pct-t.cpp new file mode 100644 index 000000000..54180b5c0 --- /dev/null +++ b/test/tap/tests/test_query_cache_soft_ttl_pct-t.cpp @@ -0,0 +1,208 @@ +/** + * @file test_query_cache_soft_ttl_pct-t.cpp + * @brief This test that query cache entries are refreshed when soft ttl is + * reached. + * @details This test configures a query rule with cache and configures the + * global variable mysql-query_cache_soft_ttl_pct. Then, caches a + * "SELECT SLEEP(1) and creates 4 threads to send this same query when the soft + * ttl have been reached. Finally, checks that only one of the threads has hit + * the hostgroup looking at how long it has taken for each thread to execute + * the query, and looking in the table "stats_mysql_query_digest" + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "proxysql_utils.h" +#include "command_line.h" +#include "utils.h" +#include "tap.h" + +using std::vector; +using std::string; + +double timer_result_one = 0; +double timer_result_two = 0; +double timer_result_three = 0; +double timer_result_four = 0; + +const string DUMMY_QUERY = "SELECT SLEEP(1)"; + +class timer { +public: + std::chrono::time_point lastTime; + timer() : lastTime(std::chrono::high_resolution_clock::now()) {} + inline double elapsed() { + std::chrono::time_point thisTime = std::chrono::high_resolution_clock::now(); + double deltaTime = std::chrono::duration(thisTime-lastTime).count(); + lastTime = thisTime; + return deltaTime; + } +}; + +void run_dummy_query( + const char* host, const char* username, const char* password, const int port, double* timer_result +) { + MYSQL* proxy_mysql = mysql_init(NULL); + + if (!mysql_real_connect(proxy_mysql, host, username, password, NULL, port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_mysql)); + *timer_result = -1.0; + return; + } + + int soft_ttl_seconds = 2; + + for (int i = 0; i < 2; i++) { + sleep(1); + + timer stopwatch; + int err = mysql_query(proxy_mysql, DUMMY_QUERY.c_str()); + if (err) { + diag("Failed to executed query `%s`", DUMMY_QUERY.c_str()); + *timer_result = -1.0; + mysql_close(proxy_mysql); + return; + } + *timer_result += stopwatch.elapsed(); + + MYSQL_RES* res = NULL; + res = mysql_store_result(proxy_mysql); + mysql_free_result(res); + } + + mysql_close(proxy_mysql); +} + +const string STATS_QUERY_DIGEST = + "SELECT hostgroup, SUM(count_star) FROM stats_mysql_query_digest " + "WHERE digest_text = 'SELECT SLEEP(?)' GROUP BY hostgroup"; + +std::map get_digest_stats_dummy_query(MYSQL* proxy_admin) { + diag("Running: %s", STATS_QUERY_DIGEST.c_str()); + mysql_query(proxy_admin, STATS_QUERY_DIGEST.c_str()); + + std::map stats {{"cache", 0}, {"hostgroups", 0}}; // {hostgroup, count_star} + + MYSQL_RES* res = mysql_store_result(proxy_admin); + + MYSQL_ROW row; + while (row = mysql_fetch_row(res)) { + if (atoi(row[0]) == -1) + stats["cache"] += atoi(row[1]); + else + stats["hostgroups"] += atoi(row[1]); + } + mysql_free_result(res); + + return stats; +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + MYSQL* proxy_admin = mysql_init(NULL); + if (!mysql_real_connect(proxy_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(proxy_admin)); + return EXIT_FAILURE; + } + + vector admin_queries = { + "UPDATE mysql_query_rules SET cache_ttl = 4000 WHERE rule_id = 2", + "LOAD MYSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=50 WHERE variable_name='mysql-query_cache_soft_ttl_pct'", + "LOAD MYSQL VARIABLES TO RUNTIME", + }; + + for (const auto &query : admin_queries) { + diag("Running: %s", query.c_str()); + MYSQL_QUERY(proxy_admin, query.c_str()); + } + + std::map stats_before = get_digest_stats_dummy_query(proxy_admin); + + MYSQL* proxy_mysql = mysql_init(NULL); + if (!mysql_real_connect(proxy_mysql, 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)); + mysql_close(proxy_admin); + return EXIT_FAILURE; + } + + diag("Running: %s", DUMMY_QUERY.c_str()); + MYSQL_QUERY(proxy_mysql, DUMMY_QUERY.c_str()); // We want to cache query "SELECT SLEEP(1)" + + MYSQL_RES* res = NULL; + res = mysql_store_result(proxy_mysql); + mysql_free_result(res); + mysql_close(proxy_mysql); + + std::thread client_one( + run_dummy_query, cl.host, cl.username, cl.password, cl.port, &timer_result_one + ); + std::thread client_two( + run_dummy_query, cl.host, cl.username, cl.password, cl.port, &timer_result_two + ); + std::thread client_three( + run_dummy_query, cl.host, cl.username, cl.password, cl.port, &timer_result_three + ); + std::thread client_four( + run_dummy_query, cl.host, cl.username, cl.password, cl.port, &timer_result_four + ); + client_one.join(); + client_two.join(); + client_three.join(); + client_four.join(); + + if ( + timer_result_one == -1.0 || + timer_result_two == -1.0 || + timer_result_three == -1.0 || + timer_result_four == -1.0 + ) { + fprintf( + stderr, "File %s, line %d, Error: one or more threads finished with errors", __FILE__, __LINE__ + ); + mysql_close(proxy_admin); + return EXIT_FAILURE; + } + + // Get the number of clients that take more 1 second or more to execute the + // query by casting double to int. + int num_slow_clients = + (int)(timer_result_one + timer_result_two + timer_result_three + timer_result_four); + int expected_num_slow_clients = 1; + ok( + num_slow_clients == expected_num_slow_clients, + "Only one client should take 1 second to execute the query. " + "Number of clients that take more than 1 second - Exp:'%d', Act:'%d'", + expected_num_slow_clients, num_slow_clients + ); + + std::map stats_after = get_digest_stats_dummy_query(proxy_admin); + + std::map expected_stats {{"cache", 7}, {"hostgroups", 2}}; + ok( + expected_stats["cache"] == stats_after["cache"] - stats_before["cache"], + "Query cache should have been hit %d times. Number of hits - Exp:'%d', Act:'%d'", + expected_stats["cache"], expected_stats["cache"], stats_after["cache"] - stats_before["cache"] + ); + ok( + expected_stats["hostgroups"] == stats_after["hostgroups"] - stats_before["hostgroups"], + "Hostgroups should have been hit %d times. Number of hits - Exp:'%d', Act:'%d'", + expected_stats["hostgroups"], expected_stats["hostgroups"], + stats_after["hostgroups"] - stats_before["hostgroups"] + ); + + return exit_status(); +}