diff --git a/deps/postgresql/handle_row_data.patch b/deps/postgresql/handle_row_data.patch index dec09fe2b..59c8e60a7 100644 --- a/deps/postgresql/handle_row_data.patch +++ b/deps/postgresql/handle_row_data.patch @@ -6,10 +6,10 @@ index 2265ab5..56883ec 100644 return conn->result; } -+int PShandleRowData(PGconn *conn, PSresult* result) { ++int PShandleRowData(PGconn *conn, bool is_first_packet, PSresult* result) { + if (!conn || !result) + return 1; -+ return psHandleRowData(conn, result); ++ return psHandleRowData(conn, is_first_packet, result); +} + diff --git src/interfaces/libpq/fe-misc.c src/interfaces/libpq/fe-misc.c @@ -60,7 +60,7 @@ diff --git src/interfaces/libpq/fe-protocol3.c src/interfaces/libpq/fe-protocol3 index 9c4aa7e..de0746c 100644 --- src/interfaces/libpq/fe-protocol3.c +++ src/interfaces/libpq/fe-protocol3.c -@@ -2299,3 +2299,105 @@ build_startup_packet(const PGconn *conn, char *packet, +@@ -2299,3 +2299,109 @@ build_startup_packet(const PGconn *conn, char *packet, return packet_len; } @@ -78,7 +78,7 @@ index 9c4aa7e..de0746c 100644 + * -1 -> Not enough data to process the message; the next call should be to PQconsumeInput. + */ +int -+psHandleRowData(PGconn *conn, PSresult* result) ++psHandleRowData(PGconn *conn, bool isFirstPacket, PSresult* result) +{ + char id; + int msgLength; @@ -122,6 +122,10 @@ index 9c4aa7e..de0746c 100644 + return 1; + } + ++ /* First data row should be skipped since it is part of PGresult, which contains row description */ ++ if (isFirstPacket) ++ return 1; ++ + if (conn->result != NULL && + conn->result->resultStatus == PGRES_TUPLES_OK) + { @@ -194,7 +198,7 @@ index c5170d1..3e3cc34 100644 extern const PGresult *PQgetResultFromPGconn(PGconn *conn); +/* ProxySQL special handler function */ -+extern int PShandleRowData(PGconn *conn, PSresult* result); ++extern int PShandleRowData(PGconn *conn, bool is_first_packet, PSresult* result); + #ifdef __cplusplus } @@ -210,7 +214,7 @@ index a951f49..e1df8b5 100644 + /* + * ProxySQL light weight routines + */ -+extern int psHandleRowData(PGconn *conn, PSresult* result); ++extern int psHandleRowData(PGconn *conn, bool is_first_packet, PSresult* result); + /* === in fe-misc.c === */ diff --git a/include/MySQL_Query_Cache.h b/include/MySQL_Query_Cache.h new file mode 100644 index 000000000..6bd822fc2 --- /dev/null +++ b/include/MySQL_Query_Cache.h @@ -0,0 +1,26 @@ +#ifndef __CLASS_MYSQL_QUERY_CACHE_H +#define __CLASS_MYSQL_QUERY_CACHE_H + +#include "proxysql.h" +#include "cpp.h" +#include "query_cache.hpp" + +typedef struct _MySQL_QC_entry : public QC_entry_t { + uint32_t column_eof_pkt_offset; + uint32_t row_eof_pkt_offset; + uint32_t ok_pkt_offset; +} MySQL_QC_entry_t; + +class MySQL_Query_Cache : public Query_Cache { +public: + MySQL_Query_Cache() = default; + ~MySQL_Query_Cache() = default; + + bool set(uint64_t user_hash, const unsigned char* kp, uint32_t kl, unsigned char* vp, uint32_t vl, + uint64_t create_ms, uint64_t curtime_ms, uint64_t expire_ms, bool deprecate_eof_active); + unsigned char* get(uint64_t user_hash, const unsigned char* kp, const uint32_t kl, uint32_t* lv, + uint64_t curtime_ms, uint64_t cache_ttl, bool deprecate_eof_active); + //void* purgeHash_thread(void*); +}; + +#endif /* __CLASS_MYSQL_QUERY_CACHE_H */ diff --git a/include/PgSQL_Data_Stream.h b/include/PgSQL_Data_Stream.h index 5b7dbc454..f13e9caba 100644 --- a/include/PgSQL_Data_Stream.h +++ b/include/PgSQL_Data_Stream.h @@ -108,8 +108,8 @@ class PgSQL_Data_Stream FixedSizeQueue data_packets_history_IN; FixedSizeQueue data_packets_history_OUT; //PtrSizeArray *PSarrayOUTpending; - PtrSizeArray* resultset; - unsigned int resultset_length; + //PtrSizeArray* resultset; + //unsigned int resultset_length; ProxySQL_Poll* mypolls; //int listener; @@ -201,8 +201,9 @@ class PgSQL_Data_Stream void check_data_flow(); int assign_fd_from_mysql_conn(); - unsigned char* resultset2buffer(bool); - void buffer2resultset(unsigned char*, unsigned int); + static unsigned char* copy_array_to_buffer(PtrSizeArray* resultset, size_t resultset_length, bool del); + static void copy_buffer_to_resultset(PtrSizeArray* resultset, unsigned char* ptr, uint64_t size, + char current_transaction_state); // safe way to attach a PgSQL Connection void attach_connection(PgSQL_Connection* mc) { diff --git a/include/PgSQL_Query_Cache.h b/include/PgSQL_Query_Cache.h new file mode 100644 index 000000000..2b695f28b --- /dev/null +++ b/include/PgSQL_Query_Cache.h @@ -0,0 +1,22 @@ +#ifndef __CLASS_PGSQL_QUERY_CACHE_H +#define __CLASS_PGSQL_QUERY_CACHE_H + +#include "proxysql.h" +#include "cpp.h" +#include "query_cache.hpp" + +typedef struct _PgSQL_QC_entry : public QC_entry_t {} PgSQL_QC_entry_t; + +class PgSQL_Query_Cache : public Query_Cache { +public: + PgSQL_Query_Cache() = default; + ~PgSQL_Query_Cache() = default; + + bool set(uint64_t user_hash, const unsigned char* kp, uint32_t kl, unsigned char* vp, uint32_t vl, + uint64_t create_ms, uint64_t curtime_ms, uint64_t expire_ms); + const std::shared_ptr get(uint64_t user_hash, const unsigned char* kp, const uint32_t kl, + uint64_t curtime_ms, uint64_t cache_ttl); + //void* purgeHash_thread(void*); +}; + +#endif /* __CLASS_PGSQL_QUERY_CACHE_H */ diff --git a/include/cpp.h b/include/cpp.h index 12f965997..e50670056 100644 --- a/include/cpp.h +++ b/include/cpp.h @@ -10,7 +10,7 @@ #include "PgSQL_Backend.h" #include "ProxySQL_Poll.h" //#include "MySQL_Data_Stream.h" -#include "query_cache.hpp" +//#include "MySQL_Query_Cache.h" #include "mysql_connection.h" #include "sqlite3db.h" //#include "StatCounters.h" diff --git a/include/gen_utils.h b/include/gen_utils.h index 4eb6e1c9c..33f5afc34 100644 --- a/include/gen_utils.h +++ b/include/gen_utils.h @@ -325,6 +325,7 @@ char *trim_spaces_in_place(char *str); char *trim_spaces_and_quotes_in_place(char *str); bool mywildcmp(const char *p, const char *str); std::string trim(const std::string& s); +char* escape_string_single_quotes_and_backslashes(char* input, bool free_it); /** * @brief Helper function that converts a MYSQL_RES into a 'SQLite3_result'. diff --git a/include/proxysql_structs.h b/include/proxysql_structs.h index 47b149081..a5ac7109f 100644 --- a/include/proxysql_structs.h +++ b/include/proxysql_structs.h @@ -706,7 +706,8 @@ class SimpleKV; class AdvancedKV; template class ProxySQL_Poll; -class Query_Cache; +class MySQL_Query_Cache; +class PgSQL_Query_Cache; class MySQL_Authentication; class MySQL_Connection; class PgSQL_Connection; @@ -1095,6 +1096,10 @@ __thread int pgsql_thread___monitor_threads; __thread char* pgsql_thread___monitor_username; __thread char* pgsql_thread___monitor_password; +// PgSQL Query Cache +__thread int pgsql_thread___query_cache_size_MB; +__thread int pgsql_thread___query_cache_soft_ttl_pct; +__thread int pgsql_thread___query_cache_handle_warnings; //--------------------------- __thread char *mysql_thread___default_schema; @@ -1382,6 +1387,10 @@ extern __thread int pgsql_thread___monitor_threads; extern __thread char* pgsql_thread___monitor_username; extern __thread char* pgsql_thread___monitor_password; +// PgSQL Query Cache +extern __thread int pgsql_thread___query_cache_size_MB; +extern __thread int pgsql_thread___query_cache_soft_ttl_pct; +extern __thread int pgsql_thread___query_cache_handle_warnings; //--------------------------- extern __thread char *mysql_thread___default_schema; diff --git a/include/query_cache.hpp b/include/query_cache.hpp index ee621d363..1659ceb9c 100644 --- a/include/query_cache.hpp +++ b/include/query_cache.hpp @@ -1,9 +1,9 @@ #ifndef __CLASS_QUERY_CACHE_H #define __CLASS_QUERY_CACHE_H - #include "proxysql.h" #include "cpp.h" -#include +#include "prometheus/counter.h" +#include "prometheus/gauge.h" #define EXPIRE_DROPIT 0 #define SHARED_QUERY_CACHE_HASH_TABLES 32 @@ -13,30 +13,6 @@ #define DEFAULT_purge_threshold_pct_min 3 #define DEFAULT_purge_threshold_pct_max 90 -#include "prometheus/counter.h" -#include "prometheus/gauge.h" - -class KV_BtreeArray; - -typedef struct __QC_entry_t QC_entry_t; - -struct __QC_entry_t { - uint64_t key; // primary key - char *value; // pointer to value - KV_BtreeArray *kv; // pointer to the KV_BtreeArray where the entry is stored - QC_entry_t *self; // pointer to itself - uint32_t klen; // length of the key : FIXME: not sure if still relevant - uint32_t length; // length of the value - 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; - uint32_t ref_count; // reference counter -}; - struct p_qc_counter { enum metric { query_cache_count_get = 0, @@ -65,34 +41,67 @@ struct qc_metrics_map_idx { }; class KV_BtreeArray; +class MySQL_Query_Cache; +class PgSQL_Query_Cache; +struct _MySQL_QC_entry; +struct _PgSQL_QC_entry; +typedef struct _MySQL_QC_entry MySQL_QC_entry_t; +typedef struct _PgSQL_QC_entry PgSQL_QC_entry_t; + +typedef struct _QC_entry { + uint64_t key; // primary key + unsigned char *value; // pointer to value + uint32_t length; // length of the value + uint32_t klen; // length of the key : FIXME: not sure if still relevant + uint64_t create_ms; // when the entry was created, monotonic, millisecond granularity + uint64_t expire_ms; // when the entry will expire, monotonic , millisecond granularity + uint64_t 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 + KV_BtreeArray* kv; // pointer to the KV_BtreeArray where the entry is stored (used for troubleshooting) + //struct _QC_entry* self; // pointer to itself +} QC_entry_t; + +template class Query_Cache { - private: - KV_BtreeArray * KVs[SHARED_QUERY_CACHE_HASH_TABLES]; - uint64_t get_data_size_total(); - unsigned int current_used_memory_pct(); - struct { - std::array p_counter_array {}; - std::array p_gauge_array {}; - } metrics; - public: + static_assert(std::is_same_v || std::is_same_v, + "Invalid QC_DERIVED Query Cache type"); + using TypeQCEntry = typename std::conditional, + MySQL_QC_entry_t, PgSQL_QC_entry_t>::type; +public: + static bool shutting_down; + static pthread_t purge_thread_id; + constexpr static unsigned int purge_loop_time = DEFAULT_purge_loop_time; + + void print_version(); + uint64_t flush(); void p_update_metrics(); - void * purgeHash_thread(void *); - int size; - int shutdown; - unsigned long long QCnow_ms; - pthread_t purge_thread_id; - unsigned int purge_loop_time; - unsigned int purge_total_time; - unsigned int purge_threshold_pct_min; - unsigned int purge_threshold_pct_max; - uint64_t max_memory_size; + SQLite3_result* SQL3_getStats(); + void purgeHash(uint64_t max_memory_size); + +protected: 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); - 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(); + + bool set(QC_entry_t* entry, uint64_t user_hash, const unsigned char *kp, uint32_t kl, unsigned char *vp, + uint32_t vl, uint64_t create_ms, uint64_t curtime_ms, uint64_t expire_ms); + std::shared_ptr get(uint64_t user_hash, const unsigned char* kp, const uint32_t kl, + uint64_t curtime_ms, uint64_t cache_ttl); + + constexpr static unsigned int purge_total_time = DEFAULT_purge_total_time; + constexpr static unsigned int purge_threshold_pct_min = DEFAULT_purge_threshold_pct_min; + constexpr static unsigned int purge_threshold_pct_max = DEFAULT_purge_threshold_pct_max; + //uint64_t max_memory_size; + +private: + KV_BtreeArray* KVs[SHARED_QUERY_CACHE_HASH_TABLES]; + uint64_t get_data_size_total(); + unsigned int current_used_memory_pct(uint64_t max_memory_size); + void purgeHash(uint64_t QCnow_ms, unsigned int curr_pct); + + struct { + std::array p_counter_array{}; + std::array p_gauge_array{}; + } metrics; }; -#endif /* __CLASS_QUERY_CACHE_H */ +#endif /* __CLASS_QUERY_CACHE_H */ diff --git a/include/query_processor.h b/include/query_processor.h index c5e0a2471..d9a4bd075 100644 --- a/include/query_processor.h +++ b/include/query_processor.h @@ -177,7 +177,7 @@ class Query_Processor_Output { mirror_flagOUT=-1; next_query_flagIN=-1; cache_ttl=-1; - cache_empty_result=1; + cache_empty_result=-1; cache_timeout=-1; reconnect=-1; timeout=-1; diff --git a/lib/Admin_Bootstrap.cpp b/lib/Admin_Bootstrap.cpp index 6bd80ffe5..8d46dfaf2 100644 --- a/lib/Admin_Bootstrap.cpp +++ b/lib/Admin_Bootstrap.cpp @@ -148,7 +148,7 @@ struct cpu_timer extern int admin_load_main_; extern bool admin_nostart_; -extern Query_Cache *GloQC; +//extern MySQL_Query_Cache *GloMyQC; extern MySQL_Authentication *GloMyAuth; extern PgSQL_Authentication *GloPgAuth; extern MySQL_LDAP_Authentication *GloMyLdapAuth; diff --git a/lib/Admin_FlushVariables.cpp b/lib/Admin_FlushVariables.cpp index 445e98c1c..c89292190 100644 --- a/lib/Admin_FlushVariables.cpp +++ b/lib/Admin_FlushVariables.cpp @@ -125,7 +125,7 @@ extern char * proxysql_version; #include "proxysql_find_charset.h" -extern Query_Cache *GloQC; +//extern MySQL_Query_Cache *GloMyQC; extern MySQL_Authentication *GloMyAuth; extern PgSQL_Authentication *GloPgAuth; extern MySQL_LDAP_Authentication *GloMyLdapAuth; diff --git a/lib/Admin_Handler.cpp b/lib/Admin_Handler.cpp index 0210d9183..266041889 100644 --- a/lib/Admin_Handler.cpp +++ b/lib/Admin_Handler.cpp @@ -77,6 +77,8 @@ using json = nlohmann::json; #include #include "PgSQL_Protocol.h" +#include "MySQL_Query_Cache.h" +#include "PgSQL_Query_Cache.h" //#include "usual/time.h" using std::string; @@ -135,7 +137,8 @@ extern bool admin_proxysql_pgsql_paused; extern int admin_old_wait_timeout; -extern Query_Cache *GloQC; +extern MySQL_Query_Cache *GloMyQC; +extern PgSQL_Query_Cache* GloPgQC; extern MySQL_Authentication *GloMyAuth; extern PgSQL_Authentication *GloPgAuth; extern MySQL_LDAP_Authentication *GloMyLdapAuth; @@ -653,13 +656,37 @@ bool admin_handler_command_proxysql(char *query_no_space, unsigned int query_no_ if (query_no_space_length==strlen("PROXYSQL FLUSH QUERY CACHE") && !strncasecmp("PROXYSQL FLUSH QUERY CACHE",query_no_space, query_no_space_length)) { proxy_info("Received PROXYSQL FLUSH QUERY CACHE command\n"); ProxySQL_Admin *SPA=(ProxySQL_Admin *)pa; - if (GloQC) { - GloQC->flush(); + if (GloMyQC) { + GloMyQC->flush(); } + //if (GloPgQC) { + // GloPgQC->flush(); + //} SPA->send_ok_msg_to_client(sess, NULL, 0, query_no_space); return false; } + if (query_no_space_length == strlen("PROXYSQL FLUSH MYSQL QUERY CACHE") && !strncasecmp("PROXYSQL FLUSH MYSQL QUERY CACHE", query_no_space, query_no_space_length)) { + proxy_info("Received PROXYSQL FLUSH MYSQL QUERY CACHE command\n"); + ProxySQL_Admin* SPA = (ProxySQL_Admin*)pa; + if (GloMyQC) { + GloMyQC->flush(); + } + SPA->send_ok_msg_to_client(sess, NULL, 0, query_no_space); + return false; + } + + if (query_no_space_length == strlen("PROXYSQL FLUSH PGSQL QUERY CACHE") && !strncasecmp("PROXYSQL FLUSH PGSQL QUERY CACHE", query_no_space, query_no_space_length)) { + proxy_info("Received PROXYSQL FLUSH PGSQL QUERY CACHE command\n"); + ProxySQL_Admin* SPA = (ProxySQL_Admin*)pa; + uint64_t count = 0; + if (GloPgQC) { + count = GloPgQC->flush(); + } + SPA->send_ok_msg_to_client(sess, NULL, (int)count, "DELETE "); + return false; + } + if (!strcasecmp("PROXYSQL FLUSH MYSQL CLIENT HOSTS", query_no_space)) { proxy_info("Received PROXYSQL FLUSH MYSQL CLIENT HOSTS command\n"); ProxySQL_Admin *SPA=(ProxySQL_Admin *)pa; diff --git a/lib/Base_Thread.cpp b/lib/Base_Thread.cpp index 50cbb0c18..66a4acdbb 100644 --- a/lib/Base_Thread.cpp +++ b/lib/Base_Thread.cpp @@ -377,9 +377,7 @@ bool Base_Thread::set_backend_to_be_skipped_if_frontend_is_slow(DS * myds, unsig // but assuming that client isn't completely blocked, we will stop checking for data // only at mysql_thread___threshold_resultset_size * 4 if constexpr (std::is_same_v) { - unsigned int buffered_data = 0; - buffered_data = myds->sess->client_myds->PSarrayOUT->len * PGSQL_RESULTSET_BUFLEN; - buffered_data += myds->sess->client_myds->resultset->len * PGSQL_RESULTSET_BUFLEN; + const unsigned int buffered_data = myds->sess->client_myds->PSarrayOUT->len * PGSQL_RESULTSET_BUFLEN; if (buffered_data > (unsigned int)pgsql_thread___threshold_resultset_size * 4) { thr->mypolls.fds[n].events = 0; return true; diff --git a/lib/ClickHouse_Server.cpp b/lib/ClickHouse_Server.cpp index 28ef34171..ae1fcfe31 100644 --- a/lib/ClickHouse_Server.cpp +++ b/lib/ClickHouse_Server.cpp @@ -441,7 +441,7 @@ static char *s_strdup(char *s) { static int __ClickHouse_Server_refresh_interval=1000; -extern Query_Cache *GloQC; +extern MySQL_Query_Cache *GloMyQC; extern ClickHouse_Authentication *GloClickHouseAuth; extern ProxySQL_Admin *GloAdmin; extern MySQL_Query_Processor* GloMyQPro; diff --git a/lib/Makefile b/lib/Makefile index 8585f4f40..504b766ee 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -147,7 +147,8 @@ _OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo Base_Session.oo Base_Thread.oo \ proxy_protocol_info.oo \ proxysql_find_charset.oo ProxySQL_Poll.oo \ - PgSQL_Protocol.oo PgSQL_Thread.oo PgSQL_Data_Stream.oo PgSQL_Session.oo PgSQL_Variables.oo PgSQL_HostGroups_Manager.oo PgSQL_Connection.oo PgSQL_Backend.oo PgSQL_Logger.oo PgSQL_Authentication.oo PgSQL_Error_Helper.oo PgSQL_Monitor.oo + PgSQL_Protocol.oo PgSQL_Thread.oo PgSQL_Data_Stream.oo PgSQL_Session.oo PgSQL_Variables.oo PgSQL_HostGroups_Manager.oo PgSQL_Connection.oo PgSQL_Backend.oo PgSQL_Logger.oo PgSQL_Authentication.oo PgSQL_Error_Helper.oo \ + MySQL_Query_Cache.oo PgSQL_Query_Cache.oo PgSQL_Monitor.oo OBJ_CXX := $(patsubst %,$(ODIR)/%,$(_OBJ_CXX)) HEADERS := ../include/*.h ../include/*.hpp diff --git a/lib/MySQL_Query_Cache.cpp b/lib/MySQL_Query_Cache.cpp new file mode 100644 index 000000000..94334e130 --- /dev/null +++ b/lib/MySQL_Query_Cache.cpp @@ -0,0 +1,316 @@ +#include "proxysql.h" +#include "cpp.h" +#include "MySQL_Protocol.h" +#include "MySQL_Query_Cache.h" + +extern MySQL_Threads_Handler* GloMTH; + +const int eof_to_ok_dif = static_cast(-(sizeof(mysql_hdr) + 5) + 2); +const int ok_to_eof_dif = static_cast(+(sizeof(mysql_hdr) + 5) - 2); + +/** + * @brief Converts a 'EOF_Packet' to holded inside a 'QC_entry_t' into a 'OK_Packet'. + * Warning: This function assumes that the supplied 'QC_entry_t' holds a valid + * 'EOF_Packet'. + * + * @param entry The 'QC_entry_t' holding a 'OK_Packet' to be converted into + * a 'EOF_Packet'. + * @return The converted packet. + */ +unsigned char* eof_to_ok_packet(const MySQL_QC_entry_t* entry) { + unsigned char* result = (unsigned char*)malloc(entry->length + eof_to_ok_dif); + unsigned char* vp = result; + unsigned char* it = entry->value; + + // Copy until the first EOF + memcpy(vp, entry->value, entry->column_eof_pkt_offset); + it += entry->column_eof_pkt_offset; + vp += entry->column_eof_pkt_offset; + + // Skip the first EOF after columns def + mysql_hdr hdr; + memcpy(&hdr, it, sizeof(mysql_hdr)); + it += sizeof(mysql_hdr) + hdr.pkt_length; + + // Copy all the rows + uint64_t u_entry_val = reinterpret_cast(entry->value); + uint64_t u_it_pos = reinterpret_cast(it); + uint64_t rows_length = (u_entry_val + entry->row_eof_pkt_offset) - u_it_pos; + memcpy(vp, it, rows_length); + vp += rows_length; + it += rows_length; + + // Replace final EOF in favor of OK packet + // ======================================= + // Copy the mysql header + memcpy(&hdr, it, sizeof(mysql_hdr)); + hdr.pkt_length = 7; + memcpy(vp, &hdr, sizeof(mysql_hdr)); + vp += sizeof(mysql_hdr); + it += sizeof(mysql_hdr); + + // OK packet header + *vp = 0xfe; + vp++; + it++; + // Initialize affected_rows and last_insert_id to zero + memset(vp, 0, 2); + vp += 2; + // Extract warning flags and status from 'EOF_packet' + unsigned char* eof_packet = entry->value + entry->row_eof_pkt_offset; + eof_packet += sizeof(mysql_hdr); + // Skip the '0xFE EOF packet header' + eof_packet += 1; + uint16_t warnings; + memcpy(&warnings, eof_packet, sizeof(uint16_t)); + eof_packet += 2; + uint16_t status_flags; + memcpy(&status_flags, eof_packet, sizeof(uint16_t)); + // Copy warnings an status flags + memcpy(vp, &status_flags, sizeof(uint16_t)); + vp += 2; + memcpy(vp, &warnings, sizeof(uint16_t)); + // ======================================= + + // Decrement ids after the first EOF + unsigned char* dp = result + entry->column_eof_pkt_offset; + mysql_hdr decrement_hdr; + for (;;) { + memcpy(&decrement_hdr, dp, sizeof(mysql_hdr)); + decrement_hdr.pkt_id--; + memcpy(dp, &decrement_hdr, sizeof(mysql_hdr)); + dp += sizeof(mysql_hdr) + decrement_hdr.pkt_length; + if (dp >= vp) + break; + } + + return result; +} + +/** + * @brief Converts a 'OK_Packet' holded inside 'QC_entry_t' into a 'EOF_Packet'. + * Warning: This function assumes that the supplied 'QC_entry_t' holds a valid + * 'OK_Packet'. + * + * @param entry The 'QC_entry_t' holding a 'EOF_Packet' to be converted into + * a 'OK_Packet'. + * @return The converted packet. + */ +unsigned char* ok_to_eof_packet(const MySQL_QC_entry_t* entry) { + unsigned char* result = (unsigned char*)malloc(entry->length + ok_to_eof_dif); + unsigned char* vp = result; + unsigned char* it = entry->value; + + // Extract warning flags and status from 'OK_packet' + unsigned char* ok_packet = it + entry->ok_pkt_offset; + mysql_hdr ok_hdr; + memcpy(&ok_hdr, ok_packet, sizeof(mysql_hdr)); + ok_packet += sizeof(mysql_hdr); + // Skip the 'OK packet header', 'affected_rows' and 'last_insert_id' + ok_packet += 3; + uint16_t status_flags; + memcpy(&status_flags, ok_packet, sizeof(uint16_t)); + ok_packet += 2; + uint16_t warnings; + memcpy(&warnings, ok_packet, sizeof(uint16_t)); + + // Find the spot in which the first EOF needs to be placed + it += sizeof(mysql_hdr); + uint64_t c_count = 0; + int c_count_len = mysql_decode_length(reinterpret_cast(it), &c_count); + it += c_count_len; + + mysql_hdr column_hdr; + for (uint64_t i = 0; i < c_count; i++) { + memcpy(&column_hdr, it, sizeof(mysql_hdr)); + it += sizeof(mysql_hdr) + column_hdr.pkt_length; + } + + // Location for 'column_eof' + uint64_t column_eof_offset = + reinterpret_cast(it) - + reinterpret_cast(entry->value); + memcpy(vp, entry->value, column_eof_offset); + vp += column_eof_offset; + + // Write 'column_eof_packet' header + column_hdr.pkt_id = column_hdr.pkt_id + 1; + column_hdr.pkt_length = 5; + memcpy(vp, &column_hdr, sizeof(mysql_hdr)); + vp += sizeof(mysql_hdr); + + // Write 'column_eof_packet' contents + *vp = 0xfe; + vp++; + memcpy(vp, &warnings, sizeof(uint16_t)); + vp += 2; + memcpy(vp, &status_flags, sizeof(uint16_t)); + vp += 2; + + // Find the OK packet + for (;;) { + mysql_hdr hdr; + memcpy(&hdr, it, sizeof(mysql_hdr)); + unsigned char* payload = + reinterpret_cast(it) + + sizeof(mysql_hdr); + + if (hdr.pkt_length < 9 && *payload == 0xfe) { + mysql_hdr ok_hdr; + ok_hdr.pkt_id = hdr.pkt_id + 1; + ok_hdr.pkt_length = 5; + memcpy(vp, &ok_hdr, sizeof(mysql_hdr)); + vp += sizeof(mysql_hdr); + + *vp = 0xfe; + vp++; + memcpy(vp, &warnings, sizeof(uint16_t)); + vp += 2; + memcpy(vp, &status_flags, sizeof(uint16_t)); + break; + } + else { + // Increment the package id by one due to 'column_eof_packet' + hdr.pkt_id += 1; + memcpy(vp, &hdr, sizeof(mysql_hdr)); + vp += sizeof(mysql_hdr); + it += sizeof(mysql_hdr); + memcpy(vp, it, hdr.pkt_length); + vp += hdr.pkt_length; + it += hdr.pkt_length; + } + } + + return result; +} + +bool MySQL_Query_Cache::set(uint64_t user_hash, const unsigned char* kp, uint32_t kl, unsigned char* vp, + uint32_t vl, uint64_t create_ms, uint64_t curtime_ms, uint64_t expire_ms, bool deprecate_eof_active) { + MySQL_QC_entry_t* entry = (MySQL_QC_entry_t*)malloc(sizeof(MySQL_QC_entry_t)); + + entry->column_eof_pkt_offset = 0; + entry->row_eof_pkt_offset = 0; + entry->ok_pkt_offset = 0; + + // Find the first EOF location + unsigned char* it = vp; + it += sizeof(mysql_hdr); + uint64_t c_count = 0; + int c_count_len = mysql_decode_length(const_cast(it), &c_count); + it += c_count_len; + + for (uint64_t i = 0; i < c_count; i++) { + mysql_hdr hdr; + memcpy(&hdr, it, sizeof(mysql_hdr)); + it += sizeof(mysql_hdr) + hdr.pkt_length; + } + + if (deprecate_eof_active == false) { + // Store EOF position and jump to rows + entry->column_eof_pkt_offset = it - vp; + mysql_hdr hdr; + memcpy(&hdr, it, sizeof(mysql_hdr)); + it += sizeof(mysql_hdr) + hdr.pkt_length; + } + + // Find the second EOF location or the OK packet + for (;;) { + mysql_hdr hdr; + memcpy(&hdr, it, sizeof(mysql_hdr)); + unsigned char* payload = it + sizeof(mysql_hdr); + + if (hdr.pkt_length < 9 && *payload == 0xfe) { + if (deprecate_eof_active) { + entry->ok_pkt_offset = it - vp; + + // Reset the warning flags to zero before storing resultset in the cache + // Reason: When a warning flag is set, it may prompt the client to invoke "SHOW WARNINGS" or "SHOW COUNT(*) FROM WARNINGS". + // However, when retrieving data from the cache, it's possible that there are no warnings present + // that might be associated with previous interactions. + unsigned char* payload_temp = payload + 1; + + // skip affected_rows + payload_temp += mysql_decode_length(payload_temp, nullptr); + + // skip last_insert_id + payload_temp += mysql_decode_length(payload_temp, nullptr); + + // skip stats_flags + payload_temp += sizeof(uint16_t); + + uint16_t warnings = 0; + memcpy(payload_temp, &warnings, sizeof(uint16_t)); + + } + else { + entry->row_eof_pkt_offset = it - vp; + + // Reset the warning flags to zero before storing resultset in the cache + // Reason: When a warning flag is set, it may prompt the client to invoke "SHOW WARNINGS" or "SHOW COUNT(*) FROM WARNINGS". + // However, when retrieving data from the cache, it's possible that there are no warnings present + // that might be associated with previous interactions. + uint16_t warnings = 0; + memcpy((payload + 1), &warnings, sizeof(uint16_t)); + } + break; + } + else { + it += sizeof(mysql_hdr) + hdr.pkt_length; + } + } + + return Query_Cache::set(entry, user_hash, kp, kl, vp, vl, create_ms, curtime_ms, expire_ms); +} + +unsigned char* MySQL_Query_Cache::get(uint64_t user_hash, const unsigned char* kp, const uint32_t kl, uint32_t* lv, + uint64_t curtime_ms, uint64_t cache_ttl, bool deprecate_eof_active) { + unsigned char* result = NULL; + + std::shared_ptr entry_shared = std::static_pointer_cast( + Query_Cache::get(user_hash, kp, kl, curtime_ms, cache_ttl) + ); + + if (entry_shared) { + if (deprecate_eof_active && entry_shared->column_eof_pkt_offset) { + result = eof_to_ok_packet(entry_shared.get()); + *lv = entry_shared->length + eof_to_ok_dif; + } + else if (!deprecate_eof_active && entry_shared->ok_pkt_offset) { + result = ok_to_eof_packet(entry_shared.get()); + *lv = entry_shared->length + ok_to_eof_dif; + } + else { + result = (unsigned char*)malloc(entry_shared->length); + memcpy(result, entry_shared->value, entry_shared->length); + *lv = entry_shared->length; + } + //__sync_fetch_and_sub(&entry->ref_count, 1); + } + return result; +} + +/*void* MySQL_Query_Cache::purgeHash_thread(void*) { + + unsigned int MySQL_Monitor__thread_MySQL_Thread_Variables_version; + MySQL_Thread* mysql_thr = new MySQL_Thread(); + MySQL_Monitor__thread_MySQL_Thread_Variables_version = GloMTH->get_global_version(); + set_thread_name("MyQCPurge"); + mysql_thr->refresh_variables(); + max_memory_size = static_cast(mysql_thread___query_cache_size_MB*1024ULL*1024ULL); + while (shutting_down == false) { + usleep(purge_loop_time); + unsigned int glover = GloMTH->get_global_version(); + if (GloMTH) { + if (MySQL_Monitor__thread_MySQL_Thread_Variables_version < glover) { + MySQL_Monitor__thread_MySQL_Thread_Variables_version = glover; + mysql_thr->refresh_variables(); + max_memory_size = static_cast(mysql_thread___query_cache_size_MB*1024ULL*1024ULL); + } + } + const unsigned int curr_pct = current_used_memory_pct(); + if (curr_pct < purge_threshold_pct_min) continue; + Query_Cache::purgeHash((monotonic_time()/1000ULL), curr_pct); + } + delete mysql_thr; + return NULL; +}*/ diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp index 490746306..a38a09065 100644 --- a/lib/MySQL_Session.cpp +++ b/lib/MySQL_Session.cpp @@ -22,7 +22,7 @@ using json = nlohmann::json; #include "SQLite3_Server.h" #include "MySQL_Variables.h" #include "ProxySQL_Cluster.hpp" - +#include "MySQL_Query_Cache.h" #include "libinjection.h" #include "libinjection_sqli.h" @@ -333,7 +333,7 @@ void* kill_query_thread(void *arg) { } extern MySQL_Query_Processor* GloMyQPro; -extern Query_Cache *GloQC; +extern MySQL_Query_Cache *GloMyQC; extern ProxySQL_Admin *GloAdmin; extern MySQL_Threads_Handler *GloMTH; @@ -6810,7 +6810,7 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C if (qpo->cache_ttl>0 && ((prepare_stmt_type & ps_type_prepare_stmt) == 0)) { bool deprecate_eof_active = client_myds->myconn->options.client_flag & CLIENT_DEPRECATE_EOF; uint32_t resbuf=0; - unsigned char *aa=GloQC->get( + unsigned char *aa= GloMyQC->get( client_myds->myconn->userinfo->hash, (const unsigned char *)CurrentQuery.QueryPointer , CurrentQuery.QueryLength , @@ -7254,18 +7254,18 @@ void MySQL_Session::MySQL_Result_to_MySQL_wire(MYSQL *mysql, MySQL_ResultSet *My unsigned char *aa=client_myds->resultset2buffer(false); while (client_myds->resultset->len) client_myds->resultset->remove_index(client_myds->resultset->len-1,NULL); bool deprecate_eof_active = client_myds->myconn->options.client_flag & CLIENT_DEPRECATE_EOF; - GloQC->set( + GloMyQC->set( client_myds->myconn->userinfo->hash , - (const unsigned char *)CurrentQuery.QueryPointer, + CurrentQuery.QueryPointer, CurrentQuery.QueryLength, - aa , + aa , // Query Cache now have the ownership, no need to free it client_myds->resultset_length , thread->curtime/1000 , thread->curtime/1000 , thread->curtime/1000 + qpo->cache_ttl, deprecate_eof_active ); - l_free(client_myds->resultset_length,aa); + //l_free(client_myds->resultset_length,aa); client_myds->resultset_length=0; } } @@ -7491,7 +7491,7 @@ void MySQL_Session::Memory_Stats() { unsigned long long internal=0; internal+=sizeof(MySQL_Session); if (qpo) - internal+=sizeof(Query_Processor_Output); + internal+=sizeof(MySQL_Query_Processor_Output); if (client_myds) { internal+=sizeof(MySQL_Data_Stream); if (client_myds->queueIN.buffer) diff --git a/lib/PgSQL_Connection.cpp b/lib/PgSQL_Connection.cpp index 2798a0a6d..d585fb96c 100644 --- a/lib/PgSQL_Connection.cpp +++ b/lib/PgSQL_Connection.cpp @@ -1609,7 +1609,7 @@ PG_ASYNC_ST PgSQL_Connection::handler(short event) { #if ENABLE_TIMER Timer timer(myds->sess->thread->Timers.Connections_Handlers); #endif // ENABLE_TIMER - unsigned long long processed_bytes = 0; // issue #527 : this variable will store the amount of bytes processed during this event + uint64_t processed_bytes = 0; // issue #527 : this variable will store the amount of bytes processed during this event if (pgsql_conn == NULL) { // it is the first time handler() is being called async_state_machine = ASYNC_CONNECT_START; @@ -1757,9 +1757,7 @@ PG_ASYNC_ST PgSQL_Connection::handler(short event) { { if (myds->sess && myds->sess->client_myds && myds->sess->mirror == false /* && myds->sess->status != SHOW_WARNINGS*/) { // see issue#4072 - unsigned int buffered_data = 0; - buffered_data = myds->sess->client_myds->PSarrayOUT->len * PGSQL_RESULTSET_BUFLEN; - buffered_data += myds->sess->client_myds->resultset->len * PGSQL_RESULTSET_BUFLEN; + const unsigned int buffered_data = myds->sess->client_myds->PSarrayOUT->len * PGSQL_RESULTSET_BUFLEN; if (buffered_data > (unsigned int)pgsql_thread___threshold_resultset_size * 8) { next_event(ASYNC_USE_RESULT_CONT); // we temporarily pause . See #1232 break; @@ -1857,8 +1855,15 @@ PG_ASYNC_ST PgSQL_Connection::handler(short event) { const unsigned int bytes_recv = query_result->add_row(result.get()); update_bytes_recv(bytes_recv); processed_bytes += bytes_recv; // issue #527 : this variable will store the amount of bytes processed during this event + + bool suspend_resultset_fetch = (processed_bytes > (unsigned int)pgsql_thread___threshold_resultset_size * 8); + + if (suspend_resultset_fetch == true && myds->sess && myds->sess->qpo && myds->sess->qpo->cache_ttl > 0) { + suspend_resultset_fetch = (processed_bytes > ((uint64_t)pgsql_thread___query_cache_size_MB) * 1024ULL * 1024ULL); + } + if ( - (processed_bytes > (unsigned int)pgsql_thread___threshold_resultset_size * 8) + suspend_resultset_fetch || (pgsql_thread___throttle_ratio_server_to_client && pgsql_thread___throttle_max_bytes_per_second_to_client && (processed_bytes > (unsigned long long)pgsql_thread___throttle_max_bytes_per_second_to_client / 10 * (unsigned long long)pgsql_thread___throttle_ratio_server_to_client)) ) { @@ -1879,8 +1884,14 @@ PG_ASYNC_ST PgSQL_Connection::handler(short event) { update_bytes_recv(bytes_recv); processed_bytes += bytes_recv; // issue #527 : this variable will store the amount of bytes processed during this event + bool suspend_resultset_fetch = (processed_bytes > (unsigned int)pgsql_thread___threshold_resultset_size * 8); + + if (suspend_resultset_fetch == true && myds->sess && myds->sess->qpo && myds->sess->qpo->cache_ttl > 0) { + suspend_resultset_fetch = (processed_bytes > ((uint64_t)pgsql_thread___query_cache_size_MB) * 1024ULL * 1024ULL); + } + if ( - (processed_bytes > (unsigned int)pgsql_thread___threshold_resultset_size * 8) + suspend_resultset_fetch || (pgsql_thread___throttle_ratio_server_to_client && pgsql_thread___throttle_max_bytes_per_second_to_client && (processed_bytes > (unsigned long long)pgsql_thread___throttle_max_bytes_per_second_to_client / 10 * (unsigned long long)pgsql_thread___throttle_ratio_server_to_client)) ) { @@ -1987,30 +1998,62 @@ void PgSQL_Connection::connect_start() { async_exit_status = PG_EVENT_NONE; std::ostringstream conninfo; - conninfo << "user=" << userinfo->username << " "; // username - conninfo << "password=" << userinfo->password << " "; // password - conninfo << "host=" << parent->address << " "; // backend address + char* escaped_str = escape_string_single_quotes_and_backslashes(userinfo->username, false); + conninfo << "user='" << escaped_str << "' "; // username + if (escaped_str != userinfo->username) + free(escaped_str); + + escaped_str = escape_string_single_quotes_and_backslashes(userinfo->password, false); + conninfo << "password='" << escaped_str << "' "; // password + if (escaped_str != userinfo->password) + free(escaped_str); + + escaped_str = escape_string_single_quotes_and_backslashes(userinfo->dbname, false); + conninfo << "dbname='" << escaped_str << "' "; + if (escaped_str != userinfo->dbname) + free(escaped_str); + + conninfo << "host='" << parent->address << "' "; // backend address conninfo << "port=" << parent->port << " "; // backend port - conninfo << "dbname=" << userinfo->dbname << " "; conninfo << "application_name=proxysql "; // application name //conninfo << "require_auth=" << AUTHENTICATION_METHOD_STR[pgsql_thread___authentication_method]; // authentication method if (parent->use_ssl) { conninfo << "sslmode=require "; // SSL required - if (pgsql_thread___ssl_p2s_key) - conninfo << "sslkey=" << pgsql_thread___ssl_p2s_key << " "; - if (pgsql_thread___ssl_p2s_cert) - conninfo << "sslcert=" << pgsql_thread___ssl_p2s_cert << " "; - if (pgsql_thread___ssl_p2s_ca) - conninfo << "sslrootcert=" << pgsql_thread___ssl_p2s_ca << " "; - if (pgsql_thread___ssl_p2s_crl) - conninfo << "sslcrl=" << pgsql_thread___ssl_p2s_crl << " "; - if (pgsql_thread___ssl_p2s_crlpath) - conninfo << "sslcrldir=" << pgsql_thread___ssl_p2s_crlpath << " "; + if (pgsql_thread___ssl_p2s_key) { + escaped_str = escape_string_single_quotes_and_backslashes(pgsql_thread___ssl_p2s_key, false); + conninfo << "sslkey='" << escaped_str << "' "; + if (escaped_str != pgsql_thread___ssl_p2s_key) + free(escaped_str); + } + if (pgsql_thread___ssl_p2s_cert) { + escaped_str = escape_string_single_quotes_and_backslashes(pgsql_thread___ssl_p2s_cert, false); + conninfo << "sslcert='" << escaped_str << "' "; + if (escaped_str != pgsql_thread___ssl_p2s_cert) + free(escaped_str); + } + if (pgsql_thread___ssl_p2s_ca) { + escaped_str = escape_string_single_quotes_and_backslashes(pgsql_thread___ssl_p2s_ca, false); + conninfo << "sslrootcert='" << escaped_str << "' "; + if (escaped_str != pgsql_thread___ssl_p2s_ca) + free(escaped_str); + } + if (pgsql_thread___ssl_p2s_crl) { + escaped_str = escape_string_single_quotes_and_backslashes(pgsql_thread___ssl_p2s_crl, false); + conninfo << "sslcrl='" << escaped_str << "' "; + if (escaped_str != pgsql_thread___ssl_p2s_crl) + free(escaped_str); + } + if (pgsql_thread___ssl_p2s_crlpath) { + escaped_str = escape_string_single_quotes_and_backslashes(pgsql_thread___ssl_p2s_crlpath, false); + conninfo << "sslcrldir='" << escaped_str << "' "; + if (escaped_str != pgsql_thread___ssl_p2s_crlpath) + free(escaped_str); + } // Only supported in PostgreSQL Server // if (pgsql_thread___ssl_p2s_cipher) // conninfo << "sslcipher=" << pgsql_thread___ssl_p2s_cipher << " "; } else { - conninfo << "sslmode=disable "; // not supporting SSL + conninfo << "sslmode='disable' "; // not supporting SSL } /*conninfo << "postgres://"; @@ -2157,7 +2200,7 @@ void PgSQL_Connection::fetch_result_cont(short event) { if (pgsql_result) return; - switch (PShandleRowData(pgsql_conn, &ps_result)) { + switch (PShandleRowData(pgsql_conn, new_result, &ps_result)) { case 0: result_type = 2; return; @@ -2185,7 +2228,7 @@ void PgSQL_Connection::fetch_result_cont(short event) { return; } - switch (PShandleRowData(pgsql_conn, &ps_result)) { + switch (PShandleRowData(pgsql_conn, new_result, &ps_result)) { case 0: result_type = 2; return; diff --git a/lib/PgSQL_Data_Stream.cpp b/lib/PgSQL_Data_Stream.cpp index f7fb1c16b..755d9de9c 100644 --- a/lib/PgSQL_Data_Stream.cpp +++ b/lib/PgSQL_Data_Stream.cpp @@ -292,13 +292,13 @@ PgSQL_Data_Stream::PgSQL_Data_Stream() { kill_type = 0; connect_tries = 0; poll_fds_idx = -1; - resultset_length = 0; + //resultset_length = 0; revents = 0; PSarrayIN = NULL; PSarrayOUT = NULL; - resultset = NULL; + //resultset = NULL; queue_init(queueIN, QUEUE_T_DEFAULT_SIZE); queue_init(queueOUT, QUEUE_T_DEFAULT_SIZE); mybe = NULL; @@ -374,13 +374,13 @@ PgSQL_Data_Stream::~PgSQL_Data_Stream() { } delete PSarrayOUT; } - if (resultset) { + /*if (resultset) { while (resultset->len) { resultset->remove_index_fast(0, &pkt); l_free(pkt.size, pkt.ptr); } delete resultset; - } + }*/ if (mypolls) mypolls->remove_index_fast(poll_fds_idx); @@ -438,7 +438,7 @@ void PgSQL_Data_Stream::init() { if (PSarrayIN == NULL) PSarrayIN = new PtrSizeArray(); if (PSarrayOUT == NULL) PSarrayOUT = new PtrSizeArray(); // if (PSarrayOUTpending==NULL) PSarrayOUTpending= new PtrSizeArray(); - if (resultset == NULL) resultset = new PtrSizeArray(); + //if (resultset == NULL) resultset = new PtrSizeArray(); if (unlikely(GloVars.global.data_packets_history_size)) { data_packets_history_IN.set_max_size(GloVars.global.data_packets_history_size); @@ -1174,7 +1174,8 @@ int PgSQL_Data_Stream::array2buffer() { return ret; } -unsigned char* PgSQL_Data_Stream::resultset2buffer(bool del) { +unsigned char* PgSQL_Data_Stream::copy_array_to_buffer(PtrSizeArray* resultset, size_t resultset_length, + bool del) { unsigned int i; unsigned int l = 0; unsigned char* mybuff = (unsigned char*)l_alloc(resultset_length); @@ -1188,52 +1189,75 @@ unsigned char* PgSQL_Data_Stream::resultset2buffer(bool del) { return mybuff; }; -void PgSQL_Data_Stream::buffer2resultset(unsigned char* ptr, unsigned int size) { - unsigned char* __ptr = ptr; - mysql_hdr hdr; - unsigned int l; - void* buff = NULL; - unsigned int bl; - unsigned int bf; - while (__ptr < ptr + size) { - memcpy(&hdr, __ptr, sizeof(mysql_hdr)); - l = hdr.pkt_length + sizeof(mysql_hdr); // amount of space we need - if (buff) { - if (bf < l) { - // we ran out of space - resultset->add(buff, bl - bf); - buff = NULL; +static inline uint32_t get_uint32(const unsigned char* ptr) { + return (static_cast(ptr[0]) << 24) | + (static_cast(ptr[1]) << 16) | + (static_cast(ptr[2]) << 8) | + static_cast(ptr[3]); +} + +void PgSQL_Data_Stream::copy_buffer_to_resultset(PtrSizeArray* resultset, unsigned char* ptr, uint64_t size, + char current_transaction_state) { + unsigned char* current_ptr = ptr; + unsigned char* buffer = NULL; + uint32_t data_len; + uint32_t buffer_len; + uint32_t remaining_space; + + while (current_ptr < ptr + size) { + uint8_t packet_type = *current_ptr; // read packet type (unused in the code) + // Read 4-byte length + /*unsigned int a = current_ptr[read_pos++]; + unsigned int b = current_ptr[read_pos++]; + unsigned int c = current_ptr[read_pos++]; + unsigned int d = current_ptr[read_pos++]; + const uint32_t packet_length = (a << 24) | (b << 16) | (c << 8) | d; + */ + data_len = get_uint32(current_ptr + 1 /*skip type*/) + 1; // total space needed, including type + + // Handle buffer space + if (buffer) { + if (remaining_space < data_len) { + // Insufficient space, add buffer to resultset + resultset->add(buffer, buffer_len - remaining_space); + buffer = NULL; } } - if (buff == NULL) { - if (__ptr + RESULTSET_BUFLEN_DS_1M <= ptr + size) { - bl = RESULTSET_BUFLEN_DS_1M; + + // Allocate new buffer if needed + if (buffer == NULL) { + // Set buffer size depending on available space + if (current_ptr + RESULTSET_BUFLEN_DS_1M <= ptr + size) { + buffer_len = RESULTSET_BUFLEN_DS_1M; } else { - bl = RESULTSET_BUFLEN_DS_16K; + buffer_len = RESULTSET_BUFLEN_DS_16K; } - if (l > bl) { - bl = l; // make sure there is the space to copy a packet + + // Ensure buffer is large enough for the current packet + if (data_len > buffer_len) { + buffer_len = data_len; } - buff = malloc(bl); - bf = bl; + + buffer = (unsigned char*)malloc(buffer_len); + remaining_space = buffer_len; + } + + // Copy data to buffer + memcpy((unsigned char*)buffer + (buffer_len - remaining_space), current_ptr, data_len); + remaining_space -= data_len; + current_ptr += data_len; + + if (packet_type == 'Z') { + // if packet is a 'Z' packet (Ready Packet), we need to overwrite trasaction state + *(buffer + (buffer_len - remaining_space) - 1) = current_transaction_state; } - memcpy((char*)buff + (bl - bf), __ptr, l); - bf -= l; - __ptr += l; - /* - l=hdr.pkt_length+sizeof(mysql_hdr); - pkt=l_alloc(l); - memcpy(pkt,__ptr,l); - resultset->add(pkt,l); - __ptr+=l; - */ } - if (buff) { - // last buffer to add - resultset->add(buff, bl - bf); + // Add the last buffer to the resultset, if any + if (buffer) { + resultset->add(buffer, buffer_len - remaining_space); } -}; +} int PgSQL_Data_Stream::array2buffer_full() { int rc = 0; diff --git a/lib/PgSQL_Protocol.cpp b/lib/PgSQL_Protocol.cpp index 1a87ff8a4..a320a8f25 100644 --- a/lib/PgSQL_Protocol.cpp +++ b/lib/PgSQL_Protocol.cpp @@ -1289,13 +1289,13 @@ bool PgSQL_Protocol::generate_ok_packet(bool send, bool ready, const char* msg, strcmp(tag, "MOVE") == 0 || strcmp(tag, "FETCH") == 0 || strcmp(tag, "COPY") == 0 || - strcmp(tag, "SELECT") == 0 || - strcmp(tag, "COPY") == 0 ) { + strcmp(tag, "SELECT") == 0) { sprintf(tmpbuf, "%s %d", tag, rows); pgpkt.write_CommandComplete(tmpbuf); } else { pgpkt.write_CommandComplete(tag); } + free(tag); if (ready == true) { pgpkt.write_ReadyForQuery(trx_state); @@ -1309,7 +1309,6 @@ bool PgSQL_Protocol::generate_ok_packet(bool send, bool ready, const char* msg, _ptr->ptr = buff.first; _ptr->size = buff.second; } - free(tag); return true; } @@ -1839,10 +1838,12 @@ void PgSQL_Query_Result::buffer_init() { void PgSQL_Query_Result::init(PgSQL_Protocol* _proto, PgSQL_Data_Stream* _myds, PgSQL_Connection* _conn) { PROXY_TRACE2(); - transfer_started = false; proto = _proto; conn = _conn; myds = _myds; + + if (conn->processing_multi_statement == false) + transfer_started = false; buffer_init(); reset(); diff --git a/lib/PgSQL_Query_Cache.cpp b/lib/PgSQL_Query_Cache.cpp new file mode 100644 index 000000000..8d62d3650 --- /dev/null +++ b/lib/PgSQL_Query_Cache.cpp @@ -0,0 +1,48 @@ +#include "proxysql.h" +#include "cpp.h" +#include "PgSQL_Query_Cache.h" + +extern PgSQL_Threads_Handler* GloPTH; + +bool PgSQL_Query_Cache::set(uint64_t user_hash, const unsigned char* kp, uint32_t kl, unsigned char* vp, + uint32_t vl, uint64_t create_ms, uint64_t curtime_ms, uint64_t expire_ms) { + + PgSQL_QC_entry_t* entry = (PgSQL_QC_entry_t*)malloc(sizeof(PgSQL_QC_entry_t)); + return Query_Cache::set(entry, user_hash, kp, kl, vp, vl, create_ms, curtime_ms, expire_ms); +} + +const std::shared_ptr PgSQL_Query_Cache::get(uint64_t user_hash, const unsigned char* kp, + const uint32_t kl, uint64_t curtime_ms, uint64_t cache_ttl) { + + const std::shared_ptr entry_shared = std::static_pointer_cast( + Query_Cache::get(user_hash, kp, kl, curtime_ms, cache_ttl) + ); + return entry_shared; +} + +/* +void* PgSQL_Query_Cache::purgeHash_thread(void*) { + + unsigned int PgSQL_Monitor__thread_PgSQL_Thread_Variables_version; + PgSQL_Thread* pgsql_thr = new PgSQL_Thread(); + PgSQL_Monitor__thread_PgSQL_Thread_Variables_version = GloPTH->get_global_version(); + set_thread_name("PgQCPurge"); + pgsql_thr->refresh_variables(); + max_memory_size = static_cast(pgsql_thread___query_cache_size_MB*1024ULL*1024ULL); + while (shutting_down == false) { + usleep(purge_loop_time); + unsigned int glover = GloPTH->get_global_version(); + if (GloPTH) { + if (PgSQL_Monitor__thread_PgSQL_Thread_Variables_version < glover) { + PgSQL_Monitor__thread_PgSQL_Thread_Variables_version = glover; + pgsql_thr->refresh_variables(); + max_memory_size = static_cast(pgsql_thread___query_cache_size_MB*1024ULL*1024ULL); + } + } + const unsigned int curr_pct = current_used_memory_pct(); + if (curr_pct < purge_threshold_pct_min) continue; + Query_Cache::purgeHash((monotonic_time()/1000ULL), curr_pct); + } + delete pgsql_thr; + return NULL; +}*/ diff --git a/lib/PgSQL_Session.cpp b/lib/PgSQL_Session.cpp index c454982d1..b33af453c 100644 --- a/lib/PgSQL_Session.cpp +++ b/lib/PgSQL_Session.cpp @@ -23,7 +23,7 @@ using json = nlohmann::json; #include "SQLite3_Server.h" #include "MySQL_Variables.h" #include "ProxySQL_Cluster.hpp" - +#include "PgSQL_Query_Cache.h" #include "libinjection.h" #include "libinjection_sqli.h" @@ -313,7 +313,7 @@ void* PgSQL_kill_query_thread(void* arg) { } extern PgSQL_Query_Processor* GloPgQPro; -extern Query_Cache* GloQC; +extern PgSQL_Query_Cache *GloPgQC; extern ProxySQL_Admin* GloAdmin; extern PgSQL_Threads_Handler* GloPTH; @@ -5831,24 +5831,21 @@ bool PgSQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C return true; } //} - /* Query Cache is not supported for PgSQL if (qpo->cache_ttl > 0 && ((prepare_stmt_type & PgSQL_ps_type_prepare_stmt) == 0)) { - bool deprecate_eof_active = client_myds->myconn->options.client_flag & CLIENT_DEPRECATE_EOF; - uint32_t resbuf = 0; - unsigned char* aa = GloQC->get( + + const std::shared_ptr pgsql_qc_entry = GloPgQC->get( client_myds->myconn->userinfo->hash, (const unsigned char*)CurrentQuery.QueryPointer, CurrentQuery.QueryLength, - &resbuf, thread->curtime / 1000, - qpo->cache_ttl, - deprecate_eof_active + qpo->cache_ttl ); - if (aa) { - client_myds->buffer2resultset(aa, resbuf); - free(aa); - client_myds->PSarrayOUT->copy_add(client_myds->resultset, 0, client_myds->resultset->len); - while (client_myds->resultset->len) client_myds->resultset->remove_index(client_myds->resultset->len - 1, NULL); + if (pgsql_qc_entry) { + // FIXME: Add Error Transaction state detection + unsigned int nTrx = NumActiveTransactions(); + PgSQL_Data_Stream::copy_buffer_to_resultset(client_myds->PSarrayOUT, + pgsql_qc_entry->value, pgsql_qc_entry->length, (nTrx ? 'T' : 'I')); + //client_myds->PSarrayOUT->copy_add(resultset, 0, resultset->len); if (transaction_persistent_hostgroup == -1) { // not active, we can change it current_hostgroup = -1; @@ -5857,7 +5854,7 @@ bool PgSQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C l_free(pkt->size, pkt->ptr); return true; } - }*/ + } __exit_set_destination_hostgroup: @@ -6232,48 +6229,46 @@ void PgSQL_Session::PgSQL_Result_to_PgSQL_wire(PgSQL_Connection* _conn, PgSQL_Da bool transfer_started = query_result->is_transfer_started(); // if there is an error, it will be false so results are not cached bool is_tuple = query_result->get_result_packet_type() == (PGSQL_QUERY_RESULT_TUPLE | PGSQL_QUERY_RESULT_COMMAND | PGSQL_QUERY_RESULT_READY); - CurrentQuery.rows_sent = query_result->get_num_rows(); + const uint64_t num_rows = query_result->get_num_rows(); + const uint64_t resultset_size = query_result->get_resultset_size(); const auto _affected_rows = query_result->get_affected_rows(); if (_affected_rows != -1) { CurrentQuery.affected_rows = _affected_rows; CurrentQuery.have_affected_rows = true; } + CurrentQuery.rows_sent = num_rows; bool resultset_completed = query_result->get_resultset(client_myds->PSarrayOUT); if (_conn->processing_multi_statement == false) assert(resultset_completed); // the resultset should always be completed if PgSQL_Result_to_PgSQL_wire is called - if (transfer_started == false) { // we have all the resultset when PgSQL_Result_to_PgSQL_wire was called + if (transfer_started == false && _conn->processing_multi_statement == false) { // we have all the resultset when PgSQL_Result_to_PgSQL_wire was called if (qpo && qpo->cache_ttl > 0 && is_tuple == true) { // the resultset should be cached - /*if (mysql_errno(pgsql) == 0 && - (mysql_warning_count(pgsql) == 0 || - mysql_thread___query_cache_handle_warnings == 1)) { // no errors + + if (_conn->is_error_present() == false && + (/* check warnings count here*/ true || + pgsql_thread___query_cache_handle_warnings == 1)) { // no errors + if ( - (qpo->cache_empty_result == 1) - || ( - (qpo->cache_empty_result == -1) - && - (thread->variables.query_cache_stores_empty_result || query_result->num_rows) + (qpo->cache_empty_result == 1) || + ( + (qpo->cache_empty_result == -1) && + (thread->variables.query_cache_stores_empty_result || num_rows) ) ) { - client_myds->resultset->copy_add(client_myds->PSarrayOUT, 0, client_myds->PSarrayOUT->len); - client_myds->resultset_length = query_result->resultset_size; - unsigned char* aa = client_myds->resultset2buffer(false); - while (client_myds->resultset->len) client_myds->resultset->remove_index(client_myds->resultset->len - 1, NULL); - bool deprecate_eof_active = client_myds->myconn->options.client_flag & CLIENT_DEPRECATE_EOF; - GloQC->set( + // Query Cache will have the ownership to buff. No need to free it here + unsigned char* buff = PgSQL_Data_Stream::copy_array_to_buffer(client_myds->PSarrayOUT, + resultset_size, false); + GloPgQC->set( client_myds->myconn->userinfo->hash, - (const unsigned char*)CurrentQuery.QueryPointer, + CurrentQuery.QueryPointer, CurrentQuery.QueryLength, - aa, - client_myds->resultset_length, + buff, + resultset_size, thread->curtime / 1000, thread->curtime / 1000, - thread->curtime / 1000 + qpo->cache_ttl, - deprecate_eof_active + thread->curtime / 1000 + qpo->cache_ttl ); - l_free(client_myds->resultset_length, aa); - client_myds->resultset_length = 0; } - }*/ + } } } } else { // if query result is empty, means there was an error before query result was generated @@ -6534,7 +6529,7 @@ void PgSQL_Session::Memory_Stats() { unsigned long long internal = 0; internal += sizeof(PgSQL_Session); if (qpo) - internal += sizeof(Query_Processor_Output); + internal += sizeof(PgSQL_Query_Processor_Output); if (client_myds) { internal += sizeof(PgSQL_Data_Stream); if (client_myds->queueIN.buffer) @@ -6552,7 +6547,7 @@ void PgSQL_Session::Memory_Stats() { internal += client_myds->PSarrayOUT->total_size(); } else { internal += client_myds->PSarrayOUT->total_size(PGSQL_RESULTSET_BUFLEN); - internal += client_myds->resultset->total_size(PGSQL_RESULTSET_BUFLEN); + //internal += client_myds->resultset->total_size(PGSQL_RESULTSET_BUFLEN); } } } diff --git a/lib/PgSQL_Thread.cpp b/lib/PgSQL_Thread.cpp index 3931eba3a..45cf4aee0 100644 --- a/lib/PgSQL_Thread.cpp +++ b/lib/PgSQL_Thread.cpp @@ -3789,9 +3789,9 @@ void PgSQL_Thread::refresh_variables() { pgsql_thread___query_processor_iterations = GloPTH->get_variable_int((char*)"query_processor_iterations"); pgsql_thread___query_processor_regex = GloPTH->get_variable_int((char*)"query_processor_regex"); - mysql_thread___query_cache_size_MB = GloPTH->get_variable_int((char*)"query_cache_size_MB"); - mysql_thread___query_cache_soft_ttl_pct = GloPTH->get_variable_int((char*)"query_cache_soft_ttl_pct"); - mysql_thread___query_cache_handle_warnings = GloPTH->get_variable_int((char*)"query_cache_handle_warnings"); + pgsql_thread___query_cache_size_MB = GloPTH->get_variable_int((char*)"query_cache_size_MB"); + pgsql_thread___query_cache_soft_ttl_pct = GloPTH->get_variable_int((char*)"query_cache_soft_ttl_pct"); + pgsql_thread___query_cache_handle_warnings = GloPTH->get_variable_int((char*)"query_cache_handle_warnings"); /* mysql_thread___max_stmts_per_connection = GloPTH->get_variable_int((char*)"max_stmts_per_connection"); mysql_thread___max_stmts_cache = GloPTH->get_variable_int((char*)"max_stmts_cache"); @@ -3932,13 +3932,14 @@ void PgSQL_Thread::refresh_variables() { pgsql_thread___query_digests_grouping_limit = (int)GloPTH->get_variable_int((char*)"query_digests_grouping_limit"); pgsql_thread___query_digests_groups_grouping_limit = (int)GloPTH->get_variable_int((char*)"query_digests_groups_grouping_limit"); pgsql_thread___query_digests_keep_comment = (bool)GloPTH->get_variable_int((char*)"query_digests_keep_comment"); + + variables.query_cache_stores_empty_result = (bool)GloPTH->get_variable_int((char*)"query_cache_stores_empty_result"); /* variables.min_num_servers_lantency_awareness = GloPTH->get_variable_int((char*)"min_num_servers_lantency_awareness"); variables.aurora_max_lag_ms_only_read_from_replicas = GloPTH->get_variable_int((char*)"aurora_max_lag_ms_only_read_from_replicas"); variables.stats_time_backend_query = (bool)GloPTH->get_variable_int((char*)"stats_time_backend_query"); variables.stats_time_query_processor = (bool)GloPTH->get_variable_int((char*)"stats_time_query_processor"); - variables.query_cache_stores_empty_result = (bool)GloPTH->get_variable_int((char*)"query_cache_stores_empty_result"); - + mysql_thread___client_session_track_gtid = (bool)GloPTH->get_variable_int((char*)"client_session_track_gtid"); #ifdef IDLE_THREADS diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index b4d010d00..2a9b7324d 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -77,6 +77,8 @@ using json = nlohmann::json; #include #include "PgSQL_Protocol.h" +#include "MySQL_Query_Cache.h" +#include "PgSQL_Query_Cache.h" //#include "usual/time.h" using std::string; @@ -307,7 +309,8 @@ bool admin_proxysql_mysql_paused = false; bool admin_proxysql_pgsql_paused = false; int admin_old_wait_timeout; -extern Query_Cache *GloQC; +extern MySQL_Query_Cache *GloMyQC; +extern PgSQL_Query_Cache* GloPgQC; extern MySQL_Authentication *GloMyAuth; extern PgSQL_Authentication *GloPgAuth; extern MySQL_LDAP_Authentication *GloMyLdapAuth; @@ -2328,8 +2331,8 @@ void * admin_main_loop(void *arg) { } } if (GloProxyStats->MySQL_Query_Cache_timetoget(curtime)) { - if (GloQC) { - SQLite3_result * resultset=GloQC->SQL3_getStats(); + if (GloMyQC) { + SQLite3_result * resultset=GloMyQC->SQL3_getStats(); if (resultset) { GloProxyStats->MySQL_Query_Cache_sets(resultset); delete resultset; @@ -2502,9 +2505,13 @@ void update_modules_metrics() { if (GloMyMon) { GloMyMon->p_update_metrics(); } - // Update query_cache metrics - if (GloQC) { - GloQC->p_update_metrics(); + // Update mysql query_cache metrics + if (GloMyQC) { + GloMyQC->p_update_metrics(); + } + // Update pgsql query_cache metrics + if (GloPgQC) { + GloPgQC->p_update_metrics(); } // Update cluster metrics if (GloProxyCluster) { diff --git a/lib/ProxySQL_Admin_Stats.cpp b/lib/ProxySQL_Admin_Stats.cpp index 16510160f..6de28f52f 100644 --- a/lib/ProxySQL_Admin_Stats.cpp +++ b/lib/ProxySQL_Admin_Stats.cpp @@ -12,7 +12,8 @@ #include "MySQL_LDAP_Authentication.hpp" #include "MySQL_PreparedStatement.h" #include "ProxySQL_Cluster.hpp" - +#include "MySQL_Query_Cache.h" +#include "PgSQL_Query_Cache.h" #include "MySQL_Query_Processor.h" #include "PgSQL_Query_Processor.h" @@ -31,7 +32,8 @@ extern bool admin_proxysql_pgsql_paused; extern MySQL_Authentication *GloMyAuth; extern PgSQL_Authentication* GloPgAuth; extern MySQL_LDAP_Authentication *GloMyLdapAuth; -extern Query_Cache *GloQC; +extern MySQL_Query_Cache *GloMyQC; +extern PgSQL_Query_Cache* GloPgQC; extern ProxySQL_Admin *GloAdmin; extern MySQL_Threads_Handler *GloMTH; extern PgSQL_Threads_Handler* GloPTH; @@ -531,7 +533,7 @@ void ProxySQL_Admin::stats___mysql_global() { free(query); } - if (GloQC && (resultset=GloQC->SQL3_getStats())) { + if (GloMyQC && (resultset= GloMyQC->SQL3_getStats())) { for (std::vector::iterator it = resultset->rows.begin() ; it != resultset->rows.end(); ++it) { SQLite3_row *r=*it; int arg_len=0; @@ -691,7 +693,7 @@ void ProxySQL_Admin::stats___pgsql_global() { free(query); }*/ - if (GloQC && (resultset = GloQC->SQL3_getStats())) { + if (GloPgQC && (resultset = GloPgQC->SQL3_getStats())) { for (std::vector::iterator it = resultset->rows.begin(); it != resultset->rows.end(); ++it) { SQLite3_row* r = *it; int arg_len = 0; diff --git a/lib/Query_Cache.cpp b/lib/Query_Cache.cpp index 002031499..5645b4b4b 100644 --- a/lib/Query_Cache.cpp +++ b/lib/Query_Cache.cpp @@ -1,12 +1,17 @@ -#include "prometheus/counter.h" #include "btree_map.h" -#include "proxysql.h" -#include "cpp.h" -#include "query_cache.hpp" #include "proxysql_atomic.h" -//#include "SpookyV2.h" +#include "prometheus/counter.h" #include "prometheus_helpers.h" -#include "MySQL_Protocol.h" +#include "query_cache.hpp" +#include "MySQL_Query_Cache.h" +#include "PgSQL_Query_Cache.h" + +#ifdef DEBUG +#define DEB "_DEBUG" +#else +#define DEB "" +#endif /* DEBUG */ +#define QUERY_CACHE_VERSION "2.0.0385" DEB #define THR_UPDATE_CNT(__a, __b, __c, __d) \ do {\ @@ -24,140 +29,236 @@ } \ } while(0) +#define DEFAULT_SQC_size 4*1024*1024 -#ifdef DEBUG -#define DEB "_DEBUG" -#else -#define DEB "" -#endif /* DEBUG */ -#define QUERY_CACHE_VERSION "1.2.0905" DEB -#define PROXYSQL_QC_PTHREAD_MUTEX - -extern MySQL_Threads_Handler *GloMTH; +#define GET_THREAD_VARIABLE(VARIABLE_NAME) \ +({((std::is_same_v) ? mysql_thread___##VARIABLE_NAME : pgsql_thread___##VARIABLE_NAME) ;}) + +__thread uint64_t __thr_cntSet = 0; +__thread uint64_t __thr_cntGet = 0; +__thread uint64_t __thr_cntGetOK = 0; +__thread uint64_t __thr_dataIN = 0; +__thread uint64_t __thr_dataOUT = 0; +__thread uint64_t __thr_num_entries = 0; +__thread uint64_t __thr_num_deleted = 0; +__thread uint64_t __thr_size_values = 0; + +static uint64_t Glo_cntSet = 0; +static uint64_t Glo_cntGet = 0; +static uint64_t Glo_cntGetOK = 0; +static uint64_t Glo_num_entries = 0; +static uint64_t Glo_dataIN = 0; +static uint64_t Glo_dataOUT = 0; +static uint64_t Glo_cntPurge = 0; +static uint64_t Glo_size_values = 0; +static uint64_t Glo_total_freed_memory = 0; + +template +bool Query_Cache::shutting_down = false; + +template +pthread_t Query_Cache::purge_thread_id; + +/*The KV_BtreeArray class is a container class that represents a key-value store + implemented using a B-tree data structure. It provides methods for performing various + operations on the key-value pairs stored in the container.*/ +class KV_BtreeArray { +public: + /** + * Constructs a new KV_BtreeArray object with the given entry size. + * + * @param entry_size The size of each entry in the key-value store. + */ + KV_BtreeArray(unsigned int entry_size); + + /** + * Destructs the KV_BtreeArray object. + */ + ~KV_BtreeArray(); -typedef btree::btree_map BtMap_cache; + /** + * Retrieves the entry with the given key from the key-value store in the KV_BtreeArray. + * If an entry with the given key exists in the store, a weak pointer to the entry will be returned. + * If an entry with the given key does not exist in the store, an empty weak pointer will be returned. + * + * @param key The key of the entry to be retrieved. + * @return A weak pointer to the entry with the given key, or an empty weak pointer if the entry does not exist. + */ + std::weak_ptr lookup(uint64_t key); + + /** + * Replaces the entry with the given key in the key-value store in the KV_BtreeArray. + * If an entry with the given key already exists in the store, it will be replaced with the new entry. + * If an entry with the given key does not exist in the store, the new entry will be added to the store. + * + * @param key The key of the entry to be replaced. + * @param entry The new entry to be added to the store. + * @return True if the entry was successfully replaced, false otherwise. (currently always true) + */ + bool replace(uint64_t key, QC_entry_t *entry); -class KV_BtreeArray { - private: -#ifdef PROXYSQL_QC_PTHREAD_MUTEX + /** + * Clears the key-value store in the KV_BtreeArray. + * If release_entries is set to true, the entries in the store will be released. + * + * @param release_entries A flag indicating whether to release the entries in the store or not. + */ + void clear(bool release_entries = false); + + /** + * Purges entries from the key-value store in the KV_BtreeArray based on the given criteria. + * If aggressive is set to true, the function will remove entries based on the access time + * of the entries, otherwise it will remove entries based on the expiration time of the entries. + * + * @param QCnow_ms The current time in milliseconds. + * @param aggressive A flag indicating whether to perform aggressive purging or not. + */ + void purge_some(uint64_t QCnow_ms, bool aggressive); + + /** + * Retrieves the total data size of the key-value store in the KV_BtreeArray. + * The data size is calculated by multiplying the number of entries in the store + * with the size of each entry, including the size of the value, pointers, and metadata. + * + * @return The total data size of the key-value store. + */ + uint64_t get_data_size() const; + + /** + * Retrieves the number of entries in the key-value store in the KV_BtreeArray. + * + * @return The number of entries in the key-value store. + */ + int count() const; + +private: pthread_rwlock_t lock; -#else - rwlock_t lock; -#endif + std::vector> entries; + using BtMap_cache = btree::btree_map>; BtMap_cache bt_map; - PtrArray *ptrArray; - uint64_t purgeChunkSize; - uint64_t purgeIdx; - bool __insert(uint64_t, void *); - uint64_t freeable_memory; - public: - uint64_t tottopurge; - KV_BtreeArray(); - ~KV_BtreeArray(); - uint64_t get_data_size(); - void purge_some(unsigned long long, bool); - int cnt(); - bool replace(uint64_t key, QC_entry_t *entry); - QC_entry_t *lookup(uint64_t key); - void empty(); + const unsigned int qc_entry_size; + + // read lock + void rdlock(); + + // write lock + void wrlock(); + + // unlock + void unlock(); + + /** + * Adds the given entry to the entries vector of the KV_BtreeArray. + * If the capacity of the entries vector is not enough to accommodate the new entry, + * it will be resized to the nearest power of 2 greater than the current size. + * + * @param entry The entry to be added to the entries vector. + */ + void add_to_entries(const std::shared_ptr& entry); + + /** + * Removes the entry at the given index from the entries vector of the KV_BtreeArray. + * If the index is out of bounds, this function does nothing. + * + * @param index The index of the entry to be removed from the entries vector. + */ + void remove_from_entries_by_index(size_t index); }; -__thread uint64_t __thr_cntSet=0; -__thread uint64_t __thr_cntGet=0; -__thread uint64_t __thr_cntGetOK=0; -__thread uint64_t __thr_dataIN=0; -__thread uint64_t __thr_dataOUT=0; -__thread uint64_t __thr_num_entries=0; -__thread uint64_t __thr_num_deleted=0; -__thread uint64_t __thr_size_values=0; -//__thread uint64_t __thr_freeable_memory=0; - -#define DEFAULT_SQC_size 4*1024*1024 - +void free_QC_Entry(QC_entry_t* entry) { + if (entry) { + free(entry->value); + free(entry); + } +} -static uint64_t Glo_cntSet=0; -static uint64_t Glo_cntGet=0; -static uint64_t Glo_cntGetOK=0; -static uint64_t Glo_num_entries=0; -static uint64_t Glo_dataIN=0; -static uint64_t Glo_dataOUT=0; -static uint64_t Glo_cntPurge=0; -static uint64_t Glo_size_values=0; -static uint64_t Glo_total_freed_memory; - -KV_BtreeArray::KV_BtreeArray() { - freeable_memory=0; - tottopurge=0; -#ifdef PROXYSQL_QC_PTHREAD_MUTEX +KV_BtreeArray::KV_BtreeArray(unsigned int entry_size) : qc_entry_size(entry_size) { pthread_rwlock_init(&lock, NULL); -#else - spinlock_rwlock_init(&lock); -#endif - ptrArray = new PtrArray; }; KV_BtreeArray::~KV_BtreeArray() { - proxy_debug(PROXY_DEBUG_QUERY_CACHE, 3, "Size of KVBtreeArray:%d , ptrArray:%u\n", cnt() , ptrArray->len); - empty(); - QC_entry_t *qce=NULL; - while (ptrArray->len) { - qce=(QC_entry_t *)ptrArray->remove_index_fast(0); - free(qce->value); - free(qce); - } - delete ptrArray; + proxy_debug(PROXY_DEBUG_QUERY_CACHE, 3, "Size of KVBtreeArray:%d , entries:%lu\n", count(), entries.size()); + clear(true); + pthread_rwlock_destroy(&lock); }; +inline void KV_BtreeArray::rdlock() { pthread_rwlock_rdlock(&lock); } +inline void KV_BtreeArray::wrlock() { pthread_rwlock_wrlock(&lock); } +inline void KV_BtreeArray::unlock() { pthread_rwlock_unlock(&lock); } -uint64_t KV_BtreeArray::get_data_size() { - uint64_t r = __sync_fetch_and_add(&Glo_num_entries,0) * (sizeof(QC_entry_t)+sizeof(QC_entry_t *)*2+sizeof(uint64_t)*2); // + __sync_fetch_and_add(&Glo_size_values,0) ; - return r; +void KV_BtreeArray::add_to_entries(const std::shared_ptr& entry) { + if (entries.capacity() <= (entries.size() + 1)) { + const unsigned int new_size = l_near_pow_2(entries.size() + 1); + entries.reserve(new_size); + } + entries.push_back(entry); +} + +void KV_BtreeArray::remove_from_entries_by_index(size_t index) { + if (index >= entries.size()) { + return; + } + + if (index != entries.size() - 1) { + std::swap(entries[index], entries.back()); + } + + entries.pop_back(); + + if ((entries.size() > MIN_ARRAY_LEN) && (entries.capacity() > entries.size() * MIN_ARRAY_DELETE_RATIO)) { + entries.shrink_to_fit(); + } +} + +uint64_t KV_BtreeArray::get_data_size() const { + uint64_t data_size = __sync_fetch_and_add(&Glo_num_entries,0) * (qc_entry_size+sizeof(QC_entry_t*)*2+sizeof(uint64_t)*2); // + __sync_fetch_and_add(&Glo_size_values,0) ; + return data_size; }; -void KV_BtreeArray::purge_some(unsigned long long QCnow_ms, bool aggressive) { - uint64_t ret=0, i, _size=0; - QC_entry_t *qce; - unsigned long long access_ms_min=0; - unsigned long long access_ms_max=0; -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_rdlock(&lock); -#else - spin_rdlock(&lock); -#endif - for (i=0; ilen;i++) { - qce=(QC_entry_t *)ptrArray->index(i); +void KV_BtreeArray::purge_some(uint64_t QCnow_ms, bool aggressive) { + uint64_t ret = 0; + uint64_t freeable_memory = 0; + uint64_t access_ms_min = std::numeric_limits::max(); + uint64_t access_ms_max = 0; + + rdlock(); + + for (const std::shared_ptr& entry_shared : entries) { + if (aggressive) { // we have been asked to do aggressive purging - if (access_ms_min==0) { - access_ms_min = qce->access_ms; + + access_ms_min = std::min(access_ms_min, entry_shared->access_ms); + access_ms_max = std::max(access_ms_max, entry_shared->access_ms); + + /* if (access_ms_min == 0) { + access_ms_min = entry_shared->access_ms; } else { - if (access_ms_min > qce->access_ms) { - access_ms_min = qce->access_ms; + if (access_ms_min > entry_shared->access_ms) { + access_ms_min = entry_shared->access_ms; } } if (access_ms_max==0) { - access_ms_max = qce->access_ms; + access_ms_max = entry_shared->access_ms; } else { - if (access_ms_max < qce->access_ms) { - access_ms_max = qce->access_ms; + if (access_ms_max < entry_shared->access_ms) { + access_ms_max = entry_shared->access_ms; } - } + }*/ } else { // no aggresssive purging , legacy algorithm - if (qce->expire_ms==EXPIRE_DROPIT || qce->expire_msexpire_ms == EXPIRE_DROPIT || entry_shared->expire_ms < QCnow_ms) { ret++; - _size+=qce->length; + freeable_memory += entry_shared->length; } } } - freeable_memory=_size; -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_unlock(&lock); -#else - spin_rdunlock(&lock); -#endif + //freeable_memory=_size; + + unlock(); + bool cond_freeable_memory=false; if (aggressive==false) { uint64_t total_freeable_memory=0; - total_freeable_memory=freeable_memory + ret * (sizeof(QC_entry_t)+sizeof(QC_entry_t *)*2+sizeof(uint64_t)*2); + total_freeable_memory=freeable_memory + ret * (qc_entry_size+sizeof(QC_entry_t*)*2+sizeof(uint64_t)*2); if ( total_freeable_memory > get_data_size()*0.01 ) { cond_freeable_memory=true; // there is memory that can be freed } @@ -166,50 +267,46 @@ void KV_BtreeArray::purge_some(unsigned long long QCnow_ms, bool aggressive) { if ( aggressive || cond_freeable_memory ) { uint64_t removed_entries=0; uint64_t freed_memory=0; - unsigned long long access_ms_lower_mark=0; + uint64_t access_ms_lower_mark=0; if (aggressive) { - access_ms_lower_mark=access_ms_min+(access_ms_max-access_ms_min)*0.1; // hardcoded for now. Remove the entries with access time in the 10% range closest to access_ms_min + access_ms_lower_mark = access_ms_min + (access_ms_max-access_ms_min) * 0.1; // hardcoded for now. Remove the entries with access time in the 10% range closest to access_ms_min } -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_wrlock(&lock); -#else - spin_wrlock(&lock); -#endif - for (i=0; ilen;i++) { - qce=(QC_entry_t *)ptrArray->index(i); + + wrlock(); + + for (size_t i = 0; i < entries.size();) { + const std::shared_ptr& entry_shared = entries[i]; bool drop_entry=false; - if (__sync_fetch_and_add(&qce->ref_count,0)<=1) { // currently not in use - if (qce->expire_ms==EXPIRE_DROPIT || qce->expire_msref_count,0)<=1) { // currently not in use + if (entry_shared.use_count() <= 1) { // we check this to avoid releasing entries that are still in use + if (entry_shared->expire_ms == EXPIRE_DROPIT || entry_shared->expire_ms < QCnow_ms) { //legacy algorithm drop_entry=true; } if (aggressive) { // we have been asked to do aggressive purging if (drop_entry==false) { // if the entry is already marked to be dropped, no further check - if (qce->access_ms < access_ms_lower_mark) { + if (entry_shared->access_ms < access_ms_lower_mark) { drop_entry=true; } } } } if (drop_entry) { - qce=(QC_entry_t *)ptrArray->remove_index_fast(i); - - btree::btree_map::iterator lookup; - lookup = bt_map.find(qce->key); - if (lookup != bt_map.end()) { + const uint32_t length = entry_shared->length; + btree::btree_map>::iterator lookup; + lookup = bt_map.find(entry_shared->key); + if (lookup != bt_map.end()) { bt_map.erase(lookup); } - i--; - freed_memory+=qce->length; + remove_from_entries_by_index(i); + freed_memory+=length; removed_entries++; - free(qce->value); - free(qce); + continue; } + i++; } -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_unlock(&lock); -#else - spin_wrunlock(&lock); -#endif + + unlock(); + THR_DECREASE_CNT(__thr_num_deleted,Glo_num_entries,removed_entries,1); if (removed_entries) { __sync_fetch_and_add(&Glo_total_freed_memory,freed_memory); @@ -219,82 +316,69 @@ void KV_BtreeArray::purge_some(unsigned long long QCnow_ms, bool aggressive) { } }; -int KV_BtreeArray::cnt() { +inline int KV_BtreeArray::count() const { return bt_map.size(); }; bool KV_BtreeArray::replace(uint64_t key, QC_entry_t *entry) { -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_wrlock(&lock); -#else - spin_wrlock(&lock); -#endif + + std::shared_ptr entry_shared(entry, &free_QC_Entry); + wrlock(); THR_UPDATE_CNT(__thr_cntSet,Glo_cntSet,1,1); THR_UPDATE_CNT(__thr_size_values,Glo_size_values,entry->length,1); THR_UPDATE_CNT(__thr_dataIN,Glo_dataIN,entry->length,1); THR_UPDATE_CNT(__thr_num_entries,Glo_num_entries,1,1); - entry->ref_count=1; - ptrArray->add(entry); - btree::btree_map::iterator lookup; - lookup = bt_map.find(key); - if (lookup != bt_map.end()) { - lookup->second->expire_ms=EXPIRE_DROPIT; - __sync_fetch_and_sub(&lookup->second->ref_count,1); + add_to_entries(entry_shared); + btree::btree_map>::iterator lookup; + lookup = bt_map.find(key); + if (lookup != bt_map.end()) { + if (std::shared_ptr found_entry_shared = lookup->second.lock()) { + found_entry_shared->expire_ms = EXPIRE_DROPIT; + } bt_map.erase(lookup); } - bt_map.insert(std::make_pair(key,entry)); -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_unlock(&lock); -#else - spin_wrunlock(&lock); -#endif + bt_map.insert({key,entry_shared}); + +#ifdef DEBUG + assert(entry_shared.use_count() == 2); // it should be 2, one for entry_shared object and one for object in entries vector +#endif /* DEBUG */ + unlock(); return true; } -QC_entry_t * KV_BtreeArray::lookup(uint64_t key) { - QC_entry_t *entry=NULL; -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_rdlock(&lock); -#else - spin_rdlock(&lock); -#endif +std::weak_ptr KV_BtreeArray::lookup(uint64_t key) { + std::weak_ptr entry_ptr; + rdlock(); THR_UPDATE_CNT(__thr_cntGet,Glo_cntGet,1,1); - btree::btree_map::iterator lookup; - lookup = bt_map.find(key); - if (lookup != bt_map.end()) { - entry=lookup->second; - __sync_fetch_and_add(&entry->ref_count,1); - } -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_unlock(&lock); -#else - spin_rdunlock(&lock); -#endif - return entry; + btree::btree_map>::iterator lookup; + lookup = bt_map.find(key); + if (lookup != bt_map.end()) { + entry_ptr = lookup->second; + //__sync_fetch_and_add(&entry->ref_count,1); + } + unlock(); + return entry_ptr; }; -void KV_BtreeArray::empty() { -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_wrlock(&lock); -#else - spin_wrlock(&lock); -#endif - btree::btree_map::iterator lookup; +void KV_BtreeArray::clear(bool release_entries) { + wrlock(); + btree::btree_map>::iterator lookup; while (bt_map.size()) { lookup = bt_map.begin(); if ( lookup != bt_map.end() ) { - lookup->second->expire_ms=EXPIRE_DROPIT; + if (std::shared_ptr found_entry_shared = lookup->second.lock()) { + found_entry_shared->expire_ms = EXPIRE_DROPIT; + } bt_map.erase(lookup); } } -#ifdef PROXYSQL_QC_PTHREAD_MUTEX - pthread_rwlock_unlock(&lock); -#else - spin_wrunlock(&lock); -#endif -}; + if (release_entries) + entries.clear(); + + unlock(); +} using metric_name = std::string; using metric_help = std::string; @@ -398,17 +482,20 @@ qc_metrics_map = std::make_tuple( } ); -uint64_t Query_Cache::get_data_size_total() { - uint64_t r=0; - int i; - for (i=0; iget_data_size(); - } - r += __sync_fetch_and_add(&Glo_size_values,0); - return r; -}; +template +uint64_t Query_Cache::get_data_size_total() { + uint64_t total_size = 0; + for (int i = 0; i < SHARED_QUERY_CACHE_HASH_TABLES; i++) { + total_size += KVs[i]->get_data_size(); + } + total_size += __sync_fetch_and_add(&Glo_size_values, 0); + return total_size; +} -unsigned int Query_Cache::current_used_memory_pct() { +template +unsigned int Query_Cache::current_used_memory_pct(uint64_t max_memory_size) { + if (max_memory_size == 0) + return 100; uint64_t cur_size=get_data_size_total(); float pctf = (float) cur_size*100/max_memory_size; if (pctf > 100) return 100; @@ -416,7 +503,8 @@ unsigned int Query_Cache::current_used_memory_pct() { return pct; } -Query_Cache::Query_Cache() { +template +Query_Cache::Query_Cache() { #ifdef DEBUG if (glovars.has_debug==false) { #else @@ -426,23 +514,22 @@ Query_Cache::Query_Cache() { exit(EXIT_FAILURE); } for (int i=0; i(qc_metrics_map, this->metrics.p_counter_array); init_prometheus_gauge_array(qc_metrics_map, this->metrics.p_gauge_array); }; -void Query_Cache::p_update_metrics() { +template +void Query_Cache::p_update_metrics() { this->metrics.p_gauge_array[p_qc_gauge::query_cache_memory_bytes]->Set(get_data_size_total()); p_update_counter(this->metrics.p_counter_array[p_qc_counter::query_cache_count_get], Glo_cntGet - Glo_cntGetOK); p_update_counter(this->metrics.p_counter_array[p_qc_counter::query_cache_count_get_ok], Glo_cntGetOK); @@ -453,209 +540,33 @@ void Query_Cache::p_update_metrics() { p_update_counter(this->metrics.p_counter_array[p_qc_counter::query_cache_entries], Glo_num_entries); } -void Query_Cache::print_version() { +template +void Query_Cache::print_version() { fprintf(stderr,"In memory Standard Query Cache (SQC) rev. %s -- %s -- %s\n", QUERY_CACHE_VERSION, __FILE__, __TIMESTAMP__); }; -Query_Cache::~Query_Cache() { - unsigned int i; - for (i=0; i +Query_Cache::~Query_Cache() { + for (unsigned int i=0; i(- (sizeof(mysql_hdr) + 5) + 2); -const int ok_to_eof_dif = static_cast(+ (sizeof(mysql_hdr) + 5) - 2); - -/** - * @brief Converts a 'EOF_Packet' to holded inside a 'QC_entry_t' into a 'OK_Packet'. - * Warning: This function assumes that the supplied 'QC_entry_t' holds a valid - * 'EOF_Packet'. - * - * @param entry The 'QC_entry_t' holding a 'OK_Packet' to be converted into - * a 'EOF_Packet'. - * @return The converted packet. - */ -unsigned char* eof_to_ok_packet(QC_entry_t* entry) { - unsigned char* result = (unsigned char*)malloc(entry->length + eof_to_ok_dif); - unsigned char* vp = result; - char* it = entry->value; - - // Copy until the first EOF - memcpy(vp, entry->value, entry->column_eof_pkt_offset); - it += entry->column_eof_pkt_offset; - vp += entry->column_eof_pkt_offset; - - // Skip the first EOF after columns def - mysql_hdr hdr; - memcpy(&hdr, it, sizeof(mysql_hdr)); - it += sizeof(mysql_hdr) + hdr.pkt_length; - - // Copy all the rows - uint64_t u_entry_val = reinterpret_cast(entry->value); - uint64_t u_it_pos = reinterpret_cast(it); - uint64_t rows_length = (u_entry_val + entry->row_eof_pkt_offset) - u_it_pos; - memcpy(vp, it, rows_length); - vp += rows_length; - it += rows_length; - - // Replace final EOF in favor of OK packet - // ======================================= - // Copy the mysql header - memcpy(&hdr, it, sizeof(mysql_hdr)); - hdr.pkt_length = 7; - memcpy(vp, &hdr, sizeof(mysql_hdr)); - vp += sizeof(mysql_hdr); - it += sizeof(mysql_hdr); - - // OK packet header - *vp = 0xfe; - vp++; - it++; - // Initialize affected_rows and last_insert_id to zero - memset(vp, 0, 2); - vp += 2; - // Extract warning flags and status from 'EOF_packet' - char* eof_packet = entry->value + entry->row_eof_pkt_offset; - eof_packet += sizeof(mysql_hdr); - // Skip the '0xFE EOF packet header' - eof_packet += 1; - uint16_t warnings; - memcpy(&warnings, eof_packet, sizeof(uint16_t)); - eof_packet += 2; - uint16_t status_flags; - memcpy(&status_flags, eof_packet, sizeof(uint16_t)); - // Copy warnings an status flags - memcpy(vp, &status_flags, sizeof(uint16_t)); - vp += 2; - memcpy(vp, &warnings, sizeof(uint16_t)); - // ======================================= - - // Decrement ids after the first EOF - unsigned char* dp = result + entry->column_eof_pkt_offset; - mysql_hdr decrement_hdr; - for (;;) { - memcpy(&decrement_hdr, dp, sizeof(mysql_hdr)); - decrement_hdr.pkt_id--; - memcpy(dp, &decrement_hdr, sizeof(mysql_hdr)); - dp += sizeof(mysql_hdr) + decrement_hdr.pkt_length; - if (dp >= vp) - break; - } - - return result; -} - -/** - * @brief Converts a 'OK_Packet' holded inside 'QC_entry_t' into a 'EOF_Packet'. - * Warning: This function assumes that the supplied 'QC_entry_t' holds a valid - * 'OK_Packet'. - * - * @param entry The 'QC_entry_t' holding a 'EOF_Packet' to be converted into - * a 'OK_Packet'. - * @return The converted packet. - */ -unsigned char* ok_to_eof_packet(QC_entry_t* entry) { - unsigned char* result = (unsigned char*)malloc(entry->length + ok_to_eof_dif); - unsigned char* vp = result; - char* it = entry->value; - - // Extract warning flags and status from 'OK_packet' - char* ok_packet = it + entry->ok_pkt_offset; - mysql_hdr ok_hdr; - memcpy(&ok_hdr, ok_packet, sizeof(mysql_hdr)); - ok_packet += sizeof(mysql_hdr); - // Skip the 'OK packet header', 'affected_rows' and 'last_insert_id' - ok_packet += 3; - uint16_t status_flags; - memcpy(&status_flags, ok_packet, sizeof(uint16_t)); - ok_packet += 2; - uint16_t warnings; - memcpy(&warnings, ok_packet, sizeof(uint16_t)); - - // Find the spot in which the first EOF needs to be placed - it += sizeof(mysql_hdr); - uint64_t c_count = 0; - int c_count_len = mysql_decode_length(reinterpret_cast(it), &c_count); - it += c_count_len; - - mysql_hdr column_hdr; - for (uint64_t i = 0; i < c_count; i++) { - memcpy(&column_hdr, it ,sizeof(mysql_hdr)); - it += sizeof(mysql_hdr) + column_hdr.pkt_length; - } - - // Location for 'column_eof' - uint64_t column_eof_offset = - reinterpret_cast(it) - - reinterpret_cast(entry->value); - memcpy(vp, entry->value, column_eof_offset); - vp += column_eof_offset; - - // Write 'column_eof_packet' header - column_hdr.pkt_id = column_hdr.pkt_id + 1; - column_hdr.pkt_length = 5; - memcpy(vp, &column_hdr, sizeof(mysql_hdr)); - vp += sizeof(mysql_hdr); - - // Write 'column_eof_packet' contents - *vp = 0xfe; - vp++; - memcpy(vp, &warnings, sizeof(uint16_t)); - vp += 2; - memcpy(vp, &status_flags, sizeof(uint16_t)); - vp += 2; - - // Find the OK packet - for (;;) { - mysql_hdr hdr; - memcpy(&hdr, it ,sizeof(mysql_hdr)); - unsigned char* payload = - reinterpret_cast(it) + - sizeof(mysql_hdr); - - if (hdr.pkt_length < 9 && *payload == 0xfe) { - mysql_hdr ok_hdr; - ok_hdr.pkt_id = hdr.pkt_id + 1; - ok_hdr.pkt_length = 5; - memcpy(vp, &ok_hdr, sizeof(mysql_hdr)); - vp += sizeof(mysql_hdr); - - *vp = 0xfe; - vp++; - memcpy(vp, &warnings, sizeof(uint16_t)); - vp += 2; - memcpy(vp, &status_flags, sizeof(uint16_t)); - break; - } else { - // Increment the package id by one due to 'column_eof_packet' - hdr.pkt_id += 1; - memcpy(vp, &hdr, sizeof(mysql_hdr)); - vp += sizeof(mysql_hdr); - it += sizeof(mysql_hdr); - memcpy(vp, it, hdr.pkt_length); - vp += hdr.pkt_length; - it += hdr.pkt_length; - } - } - - return result; -} - -unsigned char * Query_Cache::get(uint64_t user_hash, const unsigned char *kp, const uint32_t kl, uint32_t *lv, unsigned long long curtime_ms, unsigned long long cache_ttl, bool deprecate_eof_active) { - unsigned char *result=NULL; - +template +std::shared_ptr Query_Cache::get(uint64_t user_hash, const unsigned char *kp, + const uint32_t kl, uint64_t curtime_ms, uint64_t cache_ttl) { + uint64_t hk=SpookyHash::Hash64(kp, kl, user_hash); - unsigned char i=hk%SHARED_QUERY_CACHE_HASH_TABLES; + uint8_t i=hk%SHARED_QUERY_CACHE_HASH_TABLES; - QC_entry_t *entry=KVs[i]->lookup(hk); + std::shared_ptr entry_shared = KVs[i]->lookup(hk).lock(); - if (entry!=NULL) { - unsigned long long t=curtime_ms; - if (entry->expire_ms > t && entry->create_ms + cache_ttl > t) { + if (entry_shared) { + uint64_t t = curtime_ms; + if (entry_shared->expire_ms > t && entry_shared->create_ms + cache_ttl > t) { if ( - mysql_thread___query_cache_soft_ttl_pct && !entry->refreshing && - entry->create_ms + cache_ttl * mysql_thread___query_cache_soft_ttl_pct / 100 <= t + GET_THREAD_VARIABLE(query_cache_soft_ttl_pct) && !entry_shared->refreshing && + entry_shared->create_ms + cache_ttl * GET_THREAD_VARIABLE(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 @@ -663,165 +574,61 @@ unsigned char * Query_Cache::get(uint64_t user_hash, const unsigned char *kp, co // 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; + entry_shared->refreshing = true; } else { 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; + THR_UPDATE_CNT(__thr_dataOUT,Glo_dataOUT, entry_shared->length,1); + if (t > entry_shared->access_ms) entry_shared->access_ms=t; + return entry_shared; } } - __sync_fetch_and_sub(&entry->ref_count,1); } - return result; + return std::shared_ptr(nullptr); } -bool Query_Cache::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) { - QC_entry_t *entry = (QC_entry_t *)malloc(sizeof(QC_entry_t)); +template +bool Query_Cache::set(QC_entry_t* entry, uint64_t user_hash, const unsigned char *kp, uint32_t kl, + unsigned char *vp, uint32_t vl, uint64_t create_ms, uint64_t curtime_ms, uint64_t expire_ms) { entry->klen=kl; entry->length=vl; - entry->ref_count=0; - 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; - it += sizeof(mysql_hdr); - uint64_t c_count = 0; - int c_count_len = mysql_decode_length(const_cast(it), &c_count); - it += c_count_len; - - for (uint64_t i = 0; i < c_count; i++) { - mysql_hdr hdr; - memcpy(&hdr, it ,sizeof(mysql_hdr)); - it += sizeof(mysql_hdr) + hdr.pkt_length; - } - - if (deprecate_eof_active == false) { - // Store EOF position and jump to rows - entry->column_eof_pkt_offset = it - vp; - mysql_hdr hdr; - memcpy(&hdr, it, sizeof(mysql_hdr)); - it += sizeof(mysql_hdr) + hdr.pkt_length; - } - - // Find the second EOF location or the OK packet - for (;;) { - mysql_hdr hdr; - memcpy(&hdr, it ,sizeof(mysql_hdr)); - unsigned char* payload = it + sizeof(mysql_hdr); - - if (hdr.pkt_length < 9 && *payload == 0xfe) { - if (deprecate_eof_active) { - entry->ok_pkt_offset = it - vp; - - // Reset the warning flags to zero before storing resultset in the cache - // Reason: When a warning flag is set, it may prompt the client to invoke "SHOW WARNINGS" or "SHOW COUNT(*) FROM WARNINGS". - // However, when retrieving data from the cache, it's possible that there are no warnings present - // that might be associated with previous interactions. - unsigned char* payload_temp = payload+1; - - // skip affected_rows - payload_temp += mysql_decode_length(payload_temp, nullptr); - - // skip last_insert_id - payload_temp += mysql_decode_length(payload_temp, nullptr); - - // skip stats_flags - payload_temp += sizeof(uint16_t); - - uint16_t warnings = 0; - memcpy(payload_temp, &warnings, sizeof(uint16_t)); - - } else { - entry->row_eof_pkt_offset = it - vp; - - // Reset the warning flags to zero before storing resultset in the cache - // Reason: When a warning flag is set, it may prompt the client to invoke "SHOW WARNINGS" or "SHOW COUNT(*) FROM WARNINGS". - // However, when retrieving data from the cache, it's possible that there are no warnings present - // that might be associated with previous interactions. - uint16_t warnings = 0; - memcpy((payload + 1), &warnings, sizeof(uint16_t)); - } - break; - } else { - it += sizeof(mysql_hdr) + hdr.pkt_length; - } - } - - entry->value=(char *)malloc(vl); - memcpy(entry->value,vp,vl); - entry->self=entry; + // entry->value = (unsigned char*)malloc(vl); + // memcpy(entry->value, vp, vl); + entry->value = vp; // no need to allocate new memory and copy value + //entry->self=entry; entry->create_ms=create_ms; entry->access_ms=curtime_ms; entry->expire_ms=expire_ms; uint64_t hk=SpookyHash::Hash64(kp, kl, user_hash); - unsigned char i=hk%SHARED_QUERY_CACHE_HASH_TABLES; + uint8_t i=hk%SHARED_QUERY_CACHE_HASH_TABLES; entry->key=hk; + entry->kv=KVs[i]; KVs[i]->replace(hk, entry); - return true; } -uint64_t Query_Cache::flush() { - int i; +template +uint64_t Query_Cache::flush() { uint64_t total_count=0; - for (i=0; icnt(); - KVs[i]->empty(); + for (int i=0; icount(); + KVs[i]->clear(true); } return total_count; }; -void * Query_Cache::purgeHash_thread(void *) { - unsigned int i; - unsigned int MySQL_Monitor__thread_MySQL_Thread_Variables_version; - MySQL_Thread * mysql_thr = new MySQL_Thread(); - MySQL_Monitor__thread_MySQL_Thread_Variables_version=GloMTH->get_global_version(); - set_thread_name("QueryCachePurge"); - mysql_thr->refresh_variables(); - max_memory_size = (uint64_t) mysql_thread___query_cache_size_MB*1024*1024; - while (shutdown==0) { - usleep(purge_loop_time); - unsigned long long t=monotonic_time()/1000; - QCnow_ms=t; - unsigned int glover=GloMTH->get_global_version(); - if (GloMTH) { - if (MySQL_Monitor__thread_MySQL_Thread_Variables_version < glover ) { - MySQL_Monitor__thread_MySQL_Thread_Variables_version=glover; - mysql_thr->refresh_variables(); - max_memory_size = (uint64_t) mysql_thread___query_cache_size_MB*1024*1024; - } - } - unsigned int curr_pct=current_used_memory_pct(); - if (curr_pct < purge_threshold_pct_min ) continue; - for (i=0; ipurge_some(QCnow_ms, (curr_pct > purge_threshold_pct_max)); - } +template +void Query_Cache::purgeHash(uint64_t QCnow_ms, unsigned int curr_pct) { + for (int i = 0; i < SHARED_QUERY_CACHE_HASH_TABLES; i++) { + KVs[i]->purge_some(QCnow_ms, (curr_pct > purge_threshold_pct_max)); } - delete mysql_thr; - return NULL; -}; +} -SQLite3_result * Query_Cache::SQL3_getStats() { - const int colnum=2; +template +SQLite3_result* Query_Cache::SQL3_getStats() { + constexpr int colnum =2; char buf[256]; char **pta=(char **)malloc(sizeof(char *)*colnum); - //Get_Memory_Stats(); SQLite3_result *result=new SQLite3_result(colnum); result->add_column_definition(SQLITE_TEXT,"Variable_Name"); result->add_column_definition(SQLITE_TEXT,"Variable_Value"); @@ -877,3 +684,16 @@ SQLite3_result * Query_Cache::SQL3_getStats() { free(pta); return result; } + +template +void Query_Cache::purgeHash(uint64_t max_memory_size) { + const unsigned int curr_pct = current_used_memory_pct(max_memory_size); + if (curr_pct < purge_threshold_pct_min) return; + purgeHash((monotonic_time() / 1000ULL), curr_pct); +} + +template +class Query_Cache; + +template +class Query_Cache; diff --git a/lib/gen_utils.cpp b/lib/gen_utils.cpp index 31b948c7f..f28a1c1f1 100644 --- a/lib/gen_utils.cpp +++ b/lib/gen_utils.cpp @@ -277,3 +277,34 @@ std::vector split_string(const std::string& str, char delimiter) { return tokens; } + +char* escape_string_single_quotes_and_backslashes(char* input, bool free_it) { + const char* c; + int input_len = 0; + int escape_count = 0; + + for (c = input; *c != '\0'; c++) { + if ((*c == '\'') || (*c == '\\')) { + escape_count += 2; + } + input_len++; + } + + if (escape_count == 0) + return input; + + char* output = (char*)malloc(input_len + escape_count + 1); + char* p = output; + + for (c = input; *c != '\0'; c++) { + if ((*c == '\'') || (*c == '\\')) { + *(p++) = '\\'; + } + *(p++) = *c; + } + *(p++) = '\0'; + if (free_it) { + free(input); + } + return output; +} diff --git a/lib/mysql_connection.cpp b/lib/mysql_connection.cpp index 42013c1d5..a2d91caa9 100644 --- a/lib/mysql_connection.cpp +++ b/lib/mysql_connection.cpp @@ -1531,8 +1531,15 @@ MDB_ASYNC_ST MySQL_Connection::handler(short event) { } if (rows_read_inner > 1) { process_rows_in_ASYNC_STMT_EXECUTE_STORE_RESULT_CONT(processed_bytes); + + bool suspend_resultset_fetch = (processed_bytes > (unsigned int)mysql_thread___threshold_resultset_size * 8); + + if (suspend_resultset_fetch == true && myds->sess && myds->sess->qpo && myds->sess->qpo->cache_ttl > 0) { + suspend_resultset_fetch = (processed_bytes > ((uint64_t)mysql_thread___query_cache_size_MB) * 1024ULL * 1024ULL); + } + if ( - (processed_bytes > (unsigned int)mysql_thread___threshold_resultset_size*8) + suspend_resultset_fetch || ( mysql_thread___throttle_ratio_server_to_client && mysql_thread___throttle_max_bytes_per_second_to_client && (processed_bytes > (unsigned long long)mysql_thread___throttle_max_bytes_per_second_to_client/10*(unsigned long long)mysql_thread___throttle_ratio_server_to_client) ) ) { @@ -1738,8 +1745,15 @@ MDB_ASYNC_ST MySQL_Connection::handler(short event) { myds->bytes_info.bytes_recv += br; bytes_info.bytes_recv += br; processed_bytes+=br; // issue #527 : this variable will store the amount of bytes processed during this event + + bool suspend_resultset_fetch = (processed_bytes > (unsigned int)mysql_thread___threshold_resultset_size * 8); + + if (suspend_resultset_fetch == true && myds->sess && myds->sess->qpo && myds->sess->qpo->cache_ttl > 0) { + suspend_resultset_fetch = (processed_bytes > ((uint64_t)mysql_thread___query_cache_size_MB) * 1024ULL * 1024ULL); + } + if ( - (processed_bytes > (unsigned int)mysql_thread___threshold_resultset_size*8) + suspend_resultset_fetch || ( mysql_thread___throttle_ratio_server_to_client && mysql_thread___throttle_max_bytes_per_second_to_client && (processed_bytes > (unsigned long long)mysql_thread___throttle_max_bytes_per_second_to_client/10*(unsigned long long)mysql_thread___throttle_ratio_server_to_client) ) ) { diff --git a/src/SQLite3_Server.cpp b/src/SQLite3_Server.cpp index a310961ca..da0949520 100644 --- a/src/SQLite3_Server.cpp +++ b/src/SQLite3_Server.cpp @@ -99,7 +99,7 @@ static char *s_strdup(char *s) { static int __SQLite3_Server_refresh_interval=1000; -extern Query_Cache *GloQC; +extern MySQL_Query_Cache *GloMyQC; extern MySQL_Authentication *GloMyAuth; extern ProxySQL_Admin *GloAdmin; extern MySQL_Query_Processor* GloMyQPro; diff --git a/src/main.cpp b/src/main.cpp index 8cb5f854b..e715e573e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,8 @@ using json = nlohmann::json; #include "MySQL_Authentication.hpp" #include "PgSQL_Authentication.h" #include "MySQL_LDAP_Authentication.hpp" +#include "MySQL_Query_Cache.h" +#include "PgSQL_Query_Cache.h" #include "proxysql_restapi.h" #include "Web_Interface.hpp" #include "proxysql_utils.h" @@ -444,7 +446,8 @@ int listen_fd; int socket_fd; -Query_Cache *GloQC; +MySQL_Query_Cache *GloMyQC; +PgSQL_Query_Cache* GloPgQC; MySQL_Authentication *GloMyAuth; PgSQL_Authentication* GloPgAuth; MySQL_LDAP_Authentication *GloMyLdapAuth; @@ -607,11 +610,55 @@ void* pgsql_worker_thread_func_idles(void* arg) { } #endif // IDLE_THREADS -void * mysql_shared_query_cache_funct(void *arg) { - GloQC->purgeHash_thread(NULL); +void* unified_query_cache_purge_thread(void *arg) { + + set_thread_name("QCPurgeThread"); + + MySQL_Thread* mysql_thr = new MySQL_Thread(); + unsigned int MySQL_Monitor__thread_MySQL_Thread_Variables_version; + MySQL_Monitor__thread_MySQL_Thread_Variables_version = GloMTH->get_global_version(); + mysql_thr->refresh_variables(); + uint64_t mysql_max_memory_size = static_cast(mysql_thread___query_cache_size_MB * 1024ULL * 1024ULL); + + PgSQL_Thread* pgsql_thr = new PgSQL_Thread(); + unsigned int PgSQL_Monitor__thread_PgSQL_Thread_Variables_version; + PgSQL_Monitor__thread_PgSQL_Thread_Variables_version = GloPTH->get_global_version(); + pgsql_thr->refresh_variables(); + uint64_t pgsql_max_memory_size = static_cast(pgsql_thread___query_cache_size_MB * 1024ULL * 1024ULL); + + // Both MySQL and PgSQL query caches use the same shutting_down value + while (GloMyQC->shutting_down == false && GloPgQC->shutting_down == false) { + + // Both MySQL and PgSQL query caches share the same purge_loop_time value. + // Therefore, using either purge_loop_time will have no impact on the behavior. + usleep(GloMyQC->purge_loop_time); + + const unsigned int mysql_glover = GloMTH->get_global_version(); + if (MySQL_Monitor__thread_MySQL_Thread_Variables_version < mysql_glover) { + MySQL_Monitor__thread_MySQL_Thread_Variables_version = mysql_glover; + mysql_thr->refresh_variables(); + mysql_max_memory_size = static_cast(mysql_thread___query_cache_size_MB * 1024ULL * 1024ULL); + } + GloMyQC->purgeHash(mysql_max_memory_size); + + const unsigned int pgsql_glover = GloPTH->get_global_version(); + if (PgSQL_Monitor__thread_PgSQL_Thread_Variables_version < pgsql_glover) { + PgSQL_Monitor__thread_PgSQL_Thread_Variables_version = pgsql_glover; + pgsql_thr->refresh_variables(); + pgsql_max_memory_size = static_cast(pgsql_thread___query_cache_size_MB * 1024ULL * 1024ULL); + } + GloPgQC->purgeHash(pgsql_max_memory_size); + } + + delete mysql_thr; + delete pgsql_thr; return NULL; } +/*void* pgsql_shared_query_cache_funct(void* arg) { + GloPgQC->purgeHash_thread(NULL); + return NULL; +}*/ void ProxySQL_Main_process_global_variables(int argc, const char **argv) { GloVars.errorlog = NULL; @@ -815,7 +862,8 @@ void ProxySQL_Main_process_global_variables(int argc, const char **argv) { } void ProxySQL_Main_init_main_modules() { - GloQC=NULL; + GloMyQC=NULL; + GloPgQC=NULL; GloMyQPro=NULL; GloMTH=NULL; GloMyAuth=NULL; @@ -946,9 +994,15 @@ void ProxySQL_Main_init_PgSQL_Threads_Handler_module() { } void ProxySQL_Main_init_Query_Cache_module() { - GloQC = new Query_Cache(); - GloQC->print_version(); - pthread_create(&GloQC->purge_thread_id, NULL, mysql_shared_query_cache_funct , NULL); + GloMyQC = new MySQL_Query_Cache(); + GloMyQC->print_version(); + GloPgQC = new PgSQL_Query_Cache(); + GloPgQC->print_version(); + + pthread_t purge_thread_id; + pthread_create(&purge_thread_id, NULL, unified_query_cache_purge_thread, NULL); + GloMyQC->purge_thread_id = purge_thread_id; + GloPgQC->purge_thread_id = purge_thread_id; } void ProxySQL_Main_init_MySQL_Monitor_module() { @@ -1003,10 +1057,12 @@ void ProxySQL_Main_join_all_threads() { std::cerr << "GloPTH joined in "; #endif } - if (GloQC) { - GloQC->shutdown=1; + if (GloMyQC) { + GloMyQC->shutting_down=true; + } + if (GloPgQC) { + GloPgQC->shutting_down=true; } - if (GloMyMon) { GloMyMon->shutdown=true; } @@ -1032,14 +1088,33 @@ void ProxySQL_Main_join_all_threads() { std::cerr << "GloPgMon joined in "; #endif } - // join GloQC thread - if (GloQC) { + /* Unified QC Purge Thread for both MySQL and PgSQL query cache + // join GloMyQC thread + if (GloMyQC) { cpu_timer t; - pthread_join(GloQC->purge_thread_id, NULL); + pthread_join(GloMyQC->purge_thread_id, NULL); #ifdef DEBUG - std::cerr << "GloQC joined in "; + std::cerr << "GloMyQC joined in "; #endif } + // join GloPgQC thread + if (GloPgQC) { + cpu_timer t; + pthread_join(GloPgQC->purge_thread_id, NULL); +#ifdef DEBUG + std::cerr << "GloPgQC joined in "; +#endif + }*/ + if (GloMyQC || GloPgQC) { + cpu_timer t; + // The purge_thread_id is shared by both MySQL and PgSQL. + // use either one to join the thread. + pthread_join(GloMyQC->purge_thread_id, NULL); +#ifdef DEBUG + std::cerr << "GloMyQC and GloPgQC joined in "; +#endif + } + #ifdef DEBUG std::cerr << "All threads joined in "; #endif @@ -1062,12 +1137,20 @@ void ProxySQL_Main_shutdown_all_modules() { std::cerr << "GloPgMon shutdown in "; #endif } - if (GloQC) { + if (GloMyQC) { + cpu_timer t; + delete GloMyQC; + GloMyQC=NULL; +#ifdef DEBUG + std::cerr << "GloMyQC shutdown in "; +#endif + } + if (GloPgQC) { cpu_timer t; - delete GloQC; - GloQC=NULL; + delete GloPgQC; + GloPgQC=NULL; #ifdef DEBUG - std::cerr << "GloQC shutdown in "; + std::cerr << "GloPgQC shutdown in "; #endif } if (GloMyQPro) { diff --git a/test/tap/tests/galera_1_timeout_count.cpp b/test/tap/tests/galera_1_timeout_count.cpp index d5b3375ec..34cd9a90d 100644 --- a/test/tap/tests/galera_1_timeout_count.cpp +++ b/test/tap/tests/galera_1_timeout_count.cpp @@ -40,7 +40,6 @@ #define READ_ONLY_ON "\x01\x00\x00\x01\x02\x23\x00\x00\x02\x03\x64\x65\x66\x00\x00\x00\x0d\x56\x61\x72\x69\x61\x62\x6c\x65\x5f\x6e\x61\x6d\x65\x00\x0c\x21\x00\x0f\x00\x00\x00\xfd\x01\x00\x1f\x00\x00\x1b\x00\x00\x03\x03\x64\x65\x66\x00\x00\x00\x05\x56\x61\x6c\x75\x65\x00\x0c\x21\x00\x0f\x00\x00\x00\xfd\x01\x00\x1f\x00\x00\x05\x00\x00\x04\xfe\x00\x00\x02\x00\x0d\x00\x00\x05\x09\x72\x65\x61\x64\x5f\x6f\x6e\x6c\x79\x02\x4f\x4e\x05\x00\x00\x06\xfe\x00\x00\x02\x00" extern SQLite3_Server *GloSQLite3Server; -extern Query_Cache *GloQC; extern MySQL_Authentication *GloMyAuth; extern ProxySQL_Admin *GloAdmin; extern MySQL_Threads_Handler *GloMTH; diff --git a/test/tap/tests/mysql-reg_test_4723_query_cache_stores_empty_result-t.cpp b/test/tap/tests/mysql-reg_test_4723_query_cache_stores_empty_result-t.cpp new file mode 100644 index 000000000..c6471e253 --- /dev/null +++ b/test/tap/tests/mysql-reg_test_4723_query_cache_stores_empty_result-t.cpp @@ -0,0 +1,168 @@ +/** + * @file mysql-reg_test_4723_query_cache_stores_empty_result-t.cpp + * @brief This test verfies that 'mysql-query_cache_stores_empty_result' variable works as expected. + * Specifically, it ensures that when this variable is set to `0`, empty results (zero rows) + * are not cached, and when set to `1`, they are cached. + * The test verifies this behavior by comparing the metrics before and after query execution. + */ + +#include +#include + +#include "mysql.h" +#include "command_line.h" +#include "tap.h" +#include "utils.h" + +CommandLine cl; + +std::map getQueryCacheMetrics(MYSQL* proxy_admin) { + const char* query = "SELECT Variable_Name, Variable_Value FROM stats_mysql_global WHERE Variable_Name LIKE 'Query_Cache%';"; + diag("Running: %s", query); + + int query_res = mysql_query(proxy_admin, query); + if (query_res) { + BAIL_OUT("Query failed with error: %s", mysql_error(proxy_admin)); + return {}; + } + + std::map metrics{}; + MYSQL_RES* res = mysql_store_result(proxy_admin); + if (!res) { + BAIL_OUT("Failed to store result: %s", mysql_error(proxy_admin)); + return {}; + } + + MYSQL_ROW row; + while ((row = mysql_fetch_row(res))) { + metrics[row[0]] = atoi(row[1]); + } + + mysql_free_result(res); + return metrics; +} + +class TestMetrics { +public: + std::map before; + std::map after; + + void swap() { + before.swap(after); + } + + template + bool checkMetricDelta(const std::string& metric_name, int expected, BinaryOp op) { + if (before.find(metric_name) == before.end() || after.find(metric_name) == after.end()) { + diag("Metric '%s' not found in either before or after map.", metric_name.c_str()); + return false; + } + + int delta = after[metric_name] - before[metric_name]; + bool result = op(std::max(0, delta), expected); + + diag("Checking metric '%s': Expected delta %d, Actual delta %d", metric_name.c_str(), expected, delta); + ok(result, "Metric `%s` delta is correct. Expected '%d', got '%d'", metric_name.c_str(), expected, delta); + return result; + } +}; + +// Helper function for executing MySQL queries with error handling +bool exec_query(MYSQL* conn, const char* query) { + diag("Running query: %s", query); + int query_res = mysql_query(conn, query); + if (query_res) { + diag("Query failed with error: %s", mysql_error(conn)); + return false; + } + return true; +} + +int main(int argc, char** argv) { + plan(10); // Total number of tests planned + + if (cl.getEnv()) + return exit_status(); + + // Initialize and connect to ProxySQL Admin + MYSQL* proxysql_admin = mysql_init(nullptr); + if (!proxysql_admin) { + BAIL_OUT("MySQL admin init failed."); + } + + if (!mysql_real_connect(proxysql_admin, cl.host, cl.admin_username, cl.admin_password, nullptr, cl.admin_port, nullptr, 0)) { + BAIL_OUT("Failed to connect to ProxySQL Admin: %s", mysql_error(proxysql_admin)); + } + + // Initialize and connect to ProxySQL Backend + MYSQL* proxysql_backend = mysql_init(nullptr); + if (!proxysql_backend) { + BAIL_OUT("MySQL backend init failed."); + } + + if (!mysql_real_connect(proxysql_backend, cl.host, cl.username, cl.password, nullptr, cl.port, nullptr, 0)) { + BAIL_OUT("Failed to connect to ProxySQL Backend: %s", mysql_error(proxysql_backend)); + } + + // Setting up test environment + MYSQL_QUERY(proxysql_admin, "DELETE FROM mysql_query_rules"); + MYSQL_QUERY(proxysql_admin, "INSERT INTO mysql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)"); + MYSQL_QUERY(proxysql_admin, "LOAD MYSQL QUERY RULES TO RUNTIME"); + + // Disable query cache storing empty result + MYSQL_QUERY(proxysql_admin, "UPDATE global_variables SET variable_value=0 WHERE variable_name='mysql-query_cache_stores_empty_result'"); + MYSQL_QUERY(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + TestMetrics metrics; + + metrics.before = getQueryCacheMetrics(proxysql_admin); + + // Execute the test query and check the result + if (exec_query(proxysql_backend, "SELECT 1 FROM DUAL WHERE 1!=1")) { + MYSQL_RES* res = mysql_store_result(proxysql_backend); + ok(res != nullptr, "Query executed successfully."); + mysql_free_result(res); + } + else { + ok(false, "Error executing query."); + } + + // Fetch metrics after query execution + metrics.after = getQueryCacheMetrics(proxysql_admin); + + metrics.checkMetricDelta("Query_Cache_Memory_bytes", 0, std::equal_to()); + metrics.checkMetricDelta("Query_Cache_count_SET", 0, std::equal_to()); + metrics.checkMetricDelta("Query_Cache_bytes_IN", 0, std::equal_to()); + metrics.checkMetricDelta("Query_Cache_Entries", 0, std::equal_to()); + + // Swap the before and after metrics for the next phase + metrics.swap(); + + // Enable query cache storing empty result + MYSQL_QUERY(proxysql_admin, "UPDATE global_variables SET variable_value=1 WHERE variable_name='mysql-query_cache_stores_empty_result'"); + MYSQL_QUERY(proxysql_admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + // Execute the same query again and check the result + if (exec_query(proxysql_backend, "SELECT 1 FROM DUAL WHERE 1!=1")) { + MYSQL_RES* res = mysql_store_result(proxysql_backend); + ok(res != nullptr, "Query executed successfully."); + mysql_free_result(res); + } + else { + ok(false, "Error executing query."); + } + + // Fetch metrics again and check for expected changes + metrics.after = getQueryCacheMetrics(proxysql_admin); + + metrics.checkMetricDelta("Query_Cache_Memory_bytes", 1, std::greater()); + metrics.checkMetricDelta("Query_Cache_count_SET", 1, std::equal_to()); + metrics.checkMetricDelta("Query_Cache_bytes_IN", 1, std::greater()); + metrics.checkMetricDelta("Query_Cache_Entries", 1, std::equal_to()); + + // Close the connections + mysql_close(proxysql_backend); + mysql_close(proxysql_admin); + + return exit_status(); +} diff --git a/test/tap/tests/pgsql-query_cache_soft_ttl_pct-t.cpp b/test/tap/tests/pgsql-query_cache_soft_ttl_pct-t.cpp new file mode 100644 index 000000000..df37c473e --- /dev/null +++ b/test/tap/tests/pgsql-query_cache_soft_ttl_pct-t.cpp @@ -0,0 +1,217 @@ +/** + * @file pgsql-query_cache_soft_ttl_pct-t.cpp + * @brief This test verifies that query cache entries are refreshed upon reaching the soft TTL. + * @details The test sets up a query rule with caching and configures the global variable + * `pgsql-query_cache_soft_ttl_pct`. It then executes a query: "SELECT PG_SLEEP(1)", + * and creates four threads to execute this same query once the soft TTL has been reached. + * Finally, it checks that only one of the threads has hit the hostgroup by examining + * the execution time of each thread and querying the "stats_pgsql_query_digest" table. + */ +#include +#include +#include +#include +#include +#include +#include +#include "libpq-fe.h" +#include "command_line.h" +#include "tap.h" +#include "utils.h" + +#define NUM_QUERIES 3 +#define NUM_THREADS 8 + +double timer_results[NUM_THREADS]; +const char* DUMMY_QUERY = "SELECT PG_SLEEP(1)"; + +CommandLine cl; + +class Timer { +public: + Timer() : lastTime(std::chrono::high_resolution_clock::now()) {} + + double elapsed() { + auto currentTime = std::chrono::high_resolution_clock::now(); + double deltaTime = std::chrono::duration(currentTime - lastTime).count(); + lastTime = currentTime; + return deltaTime; + } + +private: + std::chrono::time_point lastTime; +}; + + +enum ConnType { + ADMIN, + BACKEND +}; + +PGconn* createNewConnection(ConnType conn_type, bool with_ssl) { + std::stringstream ss; + const char* host = (conn_type == BACKEND) ? cl.pgsql_host : cl.pgsql_admin_port; + int port = (conn_type == BACKEND) ? cl.pgsql_port : cl.pgsql_admin_port; + const char* username = (conn_type == BACKEND) ? cl.pgsql_username : cl.admin_username; + const char* password = (conn_type == BACKEND) ? cl.pgsql_password : cl.admin_password; + + ss << "host=" << host << " port=" << port; + ss << " user=" << username << " password=" << password; + ss << (with_ssl ? " sslmode=require" : " sslmode=disable"); + + PGconn* conn = PQconnectdb(ss.str().c_str()); + if (PQstatus(conn) != CONNECTION_OK) { + diag("Connection failed to '%s': %s", (conn_type == BACKEND ? "Backend" : "Admin"), PQerrorMessage(conn)); + PQfinish(conn); + return nullptr; + } + return conn; +} + +void runDummyQuery(double* timer_result) { + std::unique_ptr pg_conn(createNewConnection(BACKEND, false), &PQfinish); + if (!pg_conn) { + *timer_result = -1.0; + return; + } + + for (int i = 0; i < NUM_QUERIES; i++) { + // execute the query at 1.4 , 2.8 and 4.2 second + // Running 3 queries we verify: + // 1. the cache before the threshold + // 2. the cache after the threshold + // 3. that the cache has been refreshed + usleep(1400000); // Sleep for 1.4 seconds + + Timer stopwatch; + PGresult* res = PQexec(pg_conn.get(), DUMMY_QUERY); + if (PQresultStatus(res) != PGRES_TUPLES_OK) { + diag("Failed to execute query `%s`: %s", DUMMY_QUERY, PQerrorMessage(pg_conn.get())); + *timer_result = -1.0; + PQclear(res); + return; + } + *timer_result += stopwatch.elapsed(); + PQclear(res); + } +} + +const std::string STATS_QUERY_DIGEST = +"SELECT hostgroup, SUM(count_star) FROM stats_pgsql_query_digest " +"WHERE digest_text = 'SELECT PG_SLEEP(?)' GROUP BY hostgroup"; + +std::map getDigestStatsDummyQuery(PGconn* proxy_admin) { + diag("Running: %s", STATS_QUERY_DIGEST.c_str()); + PGresult* res = PQexec(proxy_admin, STATS_QUERY_DIGEST.c_str()); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) { + diag("Failed to execute query `%s`: %s", STATS_QUERY_DIGEST.c_str(), PQerrorMessage(proxy_admin)); + PQclear(res); + return {}; + } + + std::map stats{ {"cache", 0}, {"hostgroups", 0} }; // {hostgroup, count_star} + int nRows = PQntuples(res); + + for (int i = 0; i < nRows; i++) { + stats[atoi(PQgetvalue(res, i, 0)) == -1 ? "cache" : "hostgroups"] = atoi(PQgetvalue(res, i, 1)); + } + + diag("Queries hitting the cache: %d", stats["cache"]); + diag("Queries NOT hitting the cache: %d", stats["hostgroups"]); + PQclear(res); + return stats; +} + +void executeTests(bool with_ssl) { + std::unique_ptr pg_admin_conn(createNewConnection(ADMIN, with_ssl), &PQfinish); + if (!pg_admin_conn) return; + + std::vector admin_queries = { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=50 WHERE variable_name='pgsql-query_cache_soft_ttl_pct'", + "LOAD PGSQL VARIABLES TO RUNTIME", + }; + + for (const auto& query : admin_queries) { + diag("Running: %s", query.c_str()); + PGresult* res = PQexec(pg_admin_conn.get(), query.c_str()); + if (PQresultStatus(res) != PGRES_COMMAND_OK) { + diag("Failed to execute query `%s`: %s", query.c_str(), PQerrorMessage(pg_admin_conn.get())); + PQclear(res); + return; + } + PQclear(res); + } + + auto stats_before = getDigestStatsDummyQuery(pg_admin_conn.get()); + + std::unique_ptr pg_conn(createNewConnection(BACKEND, with_ssl), &PQfinish); + if (!pg_conn) return; + + diag("Running: %s", DUMMY_QUERY); + PGresult* res = PQexec(pg_conn.get(), DUMMY_QUERY); + if (PQresultStatus(res) != PGRES_TUPLES_OK) { + diag("Failed to execute query `%s`: %s", DUMMY_QUERY, PQerrorMessage(pg_conn.get())); + PQclear(res); + return; + } + PQclear(res); + + std::vector mythreads(NUM_THREADS); + for (unsigned int i = 0; i < NUM_THREADS; i++) { + mythreads[i] = std::thread(runDummyQuery, &timer_results[i]); + } + + for (auto& thread : mythreads) { + thread.join(); + } + + for (unsigned int i = 0; i < NUM_THREADS; i++) { + if (timer_results[i] == -1.0) { + fprintf(stderr, "Error: one or more threads finished with errors in file %s, line %d\n", __FILE__, __LINE__); + return; + } + } + + // Calculate the number of clients that took more than 1 second to execute the query + int num_slow_clients = 0; + for (unsigned int i = 0; i < NUM_THREADS; i++) { + num_slow_clients += static_cast(timer_results[i]); + } + + 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. " + "Expected: '%d', Actual: '%d'", expected_num_slow_clients, num_slow_clients); + + auto stats_after = getDigestStatsDummyQuery(pg_admin_conn.get()); + + std::map expected_stats{ {"cache", NUM_THREADS * NUM_QUERIES - 1}, {"hostgroups", 2} }; + ok( + expected_stats["cache"] == stats_after["cache"] - stats_before["cache"], + "Query cache should have been hit %d times. Number of hits - Expected: '%d', Actual: '%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 - Expected: '%d', Actual: '%d'", + expected_stats["hostgroups"], expected_stats["hostgroups"], + stats_after["hostgroups"] - stats_before["hostgroups"] + ); +} + +int main(int argc, char** argv) { + + plan(3); // Total number of tests planned + + if (cl.getEnv()) + return exit_status(); + + executeTests(false); // without SSL + + return exit_status(); +} + diff --git a/test/tap/tests/pgsql-query_cache_test-t.cpp b/test/tap/tests/pgsql-query_cache_test-t.cpp new file mode 100644 index 000000000..d9ef758cd --- /dev/null +++ b/test/tap/tests/pgsql-query_cache_test-t.cpp @@ -0,0 +1,884 @@ +/** + * @file pgsql-query_cache_test-t.cpp + * @brief This test validates the functionality and performance of PostgreSQL's query caching mechanism. + * It tests various scenarios including multi-threaded query execution, basic cache operations, + * data manipulation queries (INSERT, UPDATE, DELETE), and transaction behavior. The goal is to + * ensure that query results are cached, retrieved, and purged according to specified caching rules, + * while verifying cache-related metrics for correctness. + */ + +#include +#include +#include +#include +#include +#include "libpq-fe.h" +#include "command_line.h" +#include "tap.h" +#include "utils.h" + +CommandLine cl; + +#define NUM_THREADS 3 +#define PGSLEEP_QUERY "SELECT PG_SLEEP(1)" + +using PGConnPtr = std::unique_ptr; + +class TestMetrics { +public: + std::map before; + std::map after; + + void swap() { + before.swap(after); + } +}; + +TestMetrics metrics; + +void printQueryCacheMetrics() { + diag("Before: Query Cache Metrics"); + for (const auto& obj : metrics.before) + diag("%s : %d", obj.first.c_str(), obj.second); + diag("==========================="); + + diag("After: Query Cache Metrics"); + for (const auto& obj : metrics.after) + diag("%s : %d", obj.first.c_str(), obj.second); + diag("==========================="); +} + +class Timer { +public: + Timer() : lastTime(std::chrono::high_resolution_clock::now()) {} + + double elapsed() { + auto currentTime = std::chrono::high_resolution_clock::now(); + double deltaTime = std::chrono::duration(currentTime - lastTime).count(); + lastTime = currentTime; + return deltaTime; + } + +private: + std::chrono::time_point lastTime; +}; + +enum ConnType { + ADMIN, + BACKEND +}; + +PGConnPtr createNewConnection(ConnType conn_type, bool with_ssl) { + std::stringstream ss; + const char* host = (conn_type == BACKEND) ? cl.pgsql_host : cl.pgsql_admin_host; + int port = (conn_type == BACKEND) ? cl.pgsql_port : cl.pgsql_admin_port; + const char* username = (conn_type == BACKEND) ? cl.pgsql_username : cl.admin_username; + const char* password = (conn_type == BACKEND) ? cl.pgsql_password : cl.admin_password; + + ss << "host=" << host << " port=" << port; + ss << " user=" << username << " password=" << password; + ss << (with_ssl ? " sslmode=require" : " sslmode=disable"); + + PGconn* conn = PQconnectdb(ss.str().c_str()); + if (PQstatus(conn) != CONNECTION_OK) { + diag("Connection failed to '%s': %s", (conn_type == BACKEND ? "Backend" : "Admin"), PQerrorMessage(conn)); + PQfinish(conn); + return PGConnPtr(nullptr, &PQfinish); + } + return PGConnPtr(conn, &PQfinish); +} + +bool executeQueries(PGconn* conn, const std::vector& queries) { + for (const auto& query : queries) { + diag("Running: %s", query.c_str()); + PGresult* res = PQexec(conn, query.c_str()); + bool success = PQresultStatus(res) == PGRES_COMMAND_OK || + PQresultStatus(res) == PGRES_TUPLES_OK; + if (!success) { + diag("Failed to execute query '%s': %s", + query.c_str(), PQerrorMessage(conn)); + PQclear(res); + return false; + } + PQclear(res); + } + return true; +} + +void run_pgsleep_thread(double* timer_result) { + PGConnPtr pg_conn = createNewConnection(ConnType::BACKEND, false); + + if (!pg_conn) { + *timer_result = -1.0; + return; + } + + for (int i = 0; i < NUM_THREADS; i++) { + // execute the query at 1.4 , 2.8 and 4.2 second + // Running 3 queries we verify: + // 1. the cache before the threshold + // 2. the cache after the threshold + // 3. that the cache has been refreshed + usleep(1400000); // Sleep for 1.4 seconds + + Timer stopwatch; + if (!executeQueries(pg_conn.get(), { PGSLEEP_QUERY })) { + *timer_result = -1.0; + return; + } + *timer_result += stopwatch.elapsed(); + } +} + +template +bool checkMetricDelta(const std::string& metric_name, int expected, BinaryOp op) { + if (metrics.before.find(metric_name) == metrics.before.end() || + metrics.after.find(metric_name) == metrics.after.end()) { + BAIL_OUT("Metric '%s' not found", metric_name.c_str()); + return false; + } + + int delta = metrics.after[metric_name] - metrics.before[metric_name]; + bool result = op(std::max(0, delta), expected); + + std::string bin_op_name = typeid(BinaryOp).name(); + bin_op_name = bin_op_name.substr(3, bin_op_name.size() - 6); + + ok(result, "Metric `%s` should be '%s' %d. Actual %d", metric_name.c_str(), bin_op_name.c_str(), expected, delta); + return result; +} + +std::map getQueryCacheMetrics(PGconn* proxy_admin) { + const char* query = "SELECT Variable_Name, Variable_Value FROM stats_pgsql_global WHERE Variable_Name LIKE 'Query_Cache%';"; + diag("Running: %s", query); + PGresult* res = PQexec(proxy_admin, query); + if (PQresultStatus(res) != PGRES_TUPLES_OK) { + diag("Failed to execute query `%s`: %s", query, PQerrorMessage(proxy_admin)); + PQclear(res); + return {}; + } + + std::map metrics{}; + int nRows = PQntuples(res); + for (int i = 0; i < nRows; i++) { + const std::string& name = PQgetvalue(res, i, 0); + metrics[name] = atoi(PQgetvalue(res, i, 1)); + } + PQclear(res); + return metrics; +} + + +void execute_multi_threaded_test(PGconn* admin_conn, PGconn* conn) { + double timer_results[NUM_THREADS]; + std::vector mythreads(NUM_THREADS); + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_soft_ttl_pct'", + "LOAD PGSQL VARIABLES TO RUNTIME", + })) + return; + + if (!executeQueries(conn, { PGSLEEP_QUERY })) + return; + + for (unsigned int i = 0; i < NUM_THREADS; i++) { + mythreads[i] = std::thread(run_pgsleep_thread, &timer_results[i]); + } + + for (auto& thread : mythreads) { + thread.join(); + } + + for (unsigned int i = 0; i < NUM_THREADS; i++) { + if (timer_results[i] == -1.0) { + fprintf(stderr, "Error: one or more threads finished with errors in file %s, line %d\n", __FILE__, __LINE__); + return; + } + } + + // Calculate the number of clients that took more than 1 second to execute the query + int num_slow_clients = 0; + for (unsigned int i = 0; i < NUM_THREADS; i++) { + num_slow_clients += static_cast(timer_results[i]); + } + + int expected_num_slow_clients = 3; + ok(num_slow_clients == expected_num_slow_clients, + "3 clients should take 1 second to execute the query. " + "Expected: '%d', Actual: '%d'", expected_num_slow_clients, num_slow_clients); + + executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=256 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME" } + ); +} + +void execute_multi_threaded_purge_test(PGconn* admin_conn, PGconn* conn) { + double timer_results[NUM_THREADS]; + std::vector mythreads(NUM_THREADS); + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_soft_ttl_pct'", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; + + if (!executeQueries(conn, { PGSLEEP_QUERY })) + return; + + usleep(5000000); + + metrics.before = getQueryCacheMetrics(admin_conn); + + for (unsigned int i = 0; i < NUM_THREADS; i++) { + mythreads[i] = std::thread(run_pgsleep_thread, &timer_results[i]); + } + + for (auto& thread : mythreads) { + thread.join(); + } + + for (unsigned int i = 0; i < NUM_THREADS; i++) { + if (timer_results[i] == -1.0) { + fprintf(stderr, "Error: one or more threads finished with errors in file %s, line %d\n", __FILE__, __LINE__); + return; + } + } + + usleep(5000000); + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + // difference query cache metrics + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 9, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 9, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 1, std::greater()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 9, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=4194304 WHERE variable_name='pgsql-threshold_resultset_size'", + "UPDATE global_variables SET variable_value=256 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME" + }); +} + +void execute_basic_test(PGconn* admin_conn, PGconn* conn) { + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_soft_ttl_pct'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; + + metrics.before = getQueryCacheMetrics(admin_conn); + + if (!executeQueries(conn, { "SELECT 1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + // difference query cache metrics + checkMetricDelta<>("Query_Cache_Memory_bytes", 1, std::greater()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 1, std::greater()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 1, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(conn, { "SELECT 1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 1, std::greater()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + metrics.swap(); + + usleep(4000000); + + if (!executeQueries(conn, { "SELECT 1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 1, std::greater()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 1, std::greater()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 1, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(admin_conn, { + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; + + usleep(5000000); + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 2, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=256 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME", + })) + return; +} + +void execute_data_manipulation_test(PGconn* admin_conn, PGconn* conn) { + + // Create query rules for INSERT, DELETE, and UPDATE statements + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (3,1,'^INSERT',4000)", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (4,1,'^DELETE',4000)", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (5,1,'^UPDATE',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + })) + return; + + // Create test table + if (!executeQueries(conn, { "CREATE TABLE IF NOT EXISTS test_table(id INT PRIMARY KEY, name TEXT);" })) + return; + + metrics.before = getQueryCacheMetrics(admin_conn); + + // INSERT + if (!executeQueries(conn, { "INSERT INTO test_table(id, name) VALUES (1, 'test')" })) + return; + + // DELETE + if (!executeQueries(conn, { "DELETE FROM test_table WHERE id = 1" })) + return; + + // UPDATE + if (!executeQueries(conn, { "UPDATE test_table SET name = 'updated' WHERE id = 1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + // Validate that no cache entries were created for DML operations + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 3, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + // Clean up test table + if (!executeQueries(conn, { "DROP TABLE IF EXISTS test_table;" })) + return; + + // Clean up query rules + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME" + })) + return; +} + +void execute_threshold_resultset_size_test(PGconn* admin_conn, PGconn* conn) { + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=1024 WHERE variable_name='pgsql-threshold_resultset_size'", + "UPDATE global_variables SET variable_value=1 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME", + })) + return; + + metrics.before = getQueryCacheMetrics(admin_conn); + + if (!executeQueries(conn, { "SELECT REPEAT('X', 8197)" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + // difference query cache metrics + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + + if (!executeQueries(admin_conn, { + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME" + })) + return; + + metrics.swap(); + + if (!executeQueries(conn, { "SELECT REPEAT('X', 8197)" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + // difference query cache metrics + checkMetricDelta<>("Query_Cache_Memory_bytes", 8197, std::greater()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 8197, std::greater()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 1, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(conn, { "SELECT REPEAT('X', 8197)" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + // difference query cache metrics + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 8197, std::greater()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=4194304 WHERE variable_name='pgsql-threshold_resultset_size'", + "UPDATE global_variables SET variable_value=256 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; +} + +void execute_multi_statement_test(PGconn* admin_conn, PGconn* conn) { + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_soft_ttl_pct'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; + + metrics.before = getQueryCacheMetrics(admin_conn); + + if (!executeQueries(conn, { "SELECT 1; SELECT 2; SELECT 3;" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + // difference query cache metrics + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(conn, { "SELECT 1; SELECT 2; SELECT 3;" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=256 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; +} + +void execute_transaction_status_test(PGconn* admin_conn, PGconn* conn) { + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',10000)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_soft_ttl_pct'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; + + metrics.before = getQueryCacheMetrics(admin_conn); + + if (!executeQueries(conn, { "SELECT 1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 1, std::greater()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 1, std::greater()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 1, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(conn, { "SELECT 1" })) + return; + + ok(PQtransactionStatus(conn) == PQTRANS_IDLE, "Connection is in IDLE state, Exp:%d : Act:%d", + PQTRANS_IDLE, PQtransactionStatus(conn)); + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 1, std::greater()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(conn, { "BEGIN" })) + return; + + ok(PQtransactionStatus(conn) == PQTRANS_INTRANS, "Connection is in TRANSACTION STATE BEFORE CACHE HIT, Exp:%d : Act:%d", + PQTRANS_INTRANS, PQtransactionStatus(conn)); + + if (!executeQueries(conn, { "SELECT 1" })) + return; + + ok(PQtransactionStatus(conn) == PQTRANS_INTRANS, "Connection is in TRANSACTION STATE AFTER CACHE HIT, Exp:%d : Act:%d", + PQTRANS_INTRANS, PQtransactionStatus(conn)); + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 1, std::greater()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + if (!executeQueries(conn, { "ROLLBACK" })) + return; + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=256 WHERE variable_name='pgsql-query_cache_size_MB'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; +} + +void execute_query_cache_store_empty_result_test(PGconn* admin_conn, PGconn* conn) { + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl,cache_empty_result) VALUES (2,1,'^SELECT',4000,0)", + "LOAD PGSQL QUERY RULES TO RUNTIME", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_soft_ttl_pct'", + "UPDATE global_variables SET variable_value=0 WHERE variable_name='pgsql-query_cache_stores_empty_result'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; + + metrics.before = getQueryCacheMetrics(admin_conn); + + if (!executeQueries(conn, {"SELECT 1 WHERE 1!=1"})) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl,cache_empty_result) VALUES (3,1,'^SELECT',4000,1)", + "LOAD PGSQL QUERY RULES TO RUNTIME" + })) + return; + + if (!executeQueries(conn, { "SELECT 1 WHERE 1!=1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 1, std::greater()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 1, std::greater()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 1, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(conn, { "SELECT 1 WHERE 1!=1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 1, std::greater()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + usleep(5000000); // Sleep for 5 seconds + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (4,1,'^SELECT',4000)", + "LOAD PGSQL QUERY RULES TO RUNTIME" + })) + return; + + metrics.before = getQueryCacheMetrics(admin_conn); + + if (!executeQueries(conn, { "SELECT 1 WHERE 1!=1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(admin_conn, { + "UPDATE global_variables SET variable_value=1 WHERE variable_name='pgsql-query_cache_stores_empty_result'", + "LOAD PGSQL VARIABLES TO RUNTIME" + })) + return; + + if (!executeQueries(conn, { "SELECT 1 WHERE 1!=1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 1, std::greater()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 1, std::greater()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 1, std::equal_to()); + + metrics.swap(); + + if (!executeQueries(conn, { "SELECT 1 WHERE 1!=1" })) + return; + + metrics.after = getQueryCacheMetrics(admin_conn); + + printQueryCacheMetrics(); + + checkMetricDelta<>("Query_Cache_Memory_bytes", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_GET_OK", 1, std::equal_to()); + checkMetricDelta<>("Query_Cache_count_SET", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_IN", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_bytes_OUT", 1, std::greater()); + checkMetricDelta<>("Query_Cache_Purged", 0, std::equal_to()); + checkMetricDelta<>("Query_Cache_Entries", 0, std::equal_to()); + + + if (!executeQueries(admin_conn, { + "DELETE FROM pgsql_query_rules", + "LOAD PGSQL QUERY RULES TO RUNTIME", + })) + return; +} + +std::vector> tests = { + { "Basic Test", execute_basic_test }, + { "Data Manipulation Test", execute_data_manipulation_test }, + { "Multi Statement Test", execute_multi_statement_test }, + { "Threshold Resultset Size Test", execute_threshold_resultset_size_test }, + { "Multi Threaded Test", execute_multi_threaded_test }, + { "Multi Threaded Purge Test", execute_multi_threaded_purge_test }, + { "Transaction Status Test", execute_transaction_status_test }, + { "Query Cache Store Empty Result Test", execute_query_cache_store_empty_result_test } +}; + +void execute_tests(bool with_ssl, bool diff_conn) { + + if (diff_conn == false) { + PGConnPtr admin_conn = createNewConnection(ConnType::ADMIN, with_ssl); + PGConnPtr backend_conn = createNewConnection(ConnType::BACKEND, with_ssl); + + if (!admin_conn || !backend_conn) { + fprintf(stderr, "Error: failed to connect to the database in file %s, line %d\n", __FILE__, __LINE__); + return; + } + + if (!executeQueries(admin_conn.get(), { "PROXYSQL FLUSH PGSQL QUERY CACHE" })) + return; + + for (const auto& test : tests) { + diag(">>>> Running %s - Shared Connection: %s <<<<", test.first.c_str(), !diff_conn ? "True" : "False"); + test.second(admin_conn.get(), backend_conn.get()); + diag(">>>> Done <<<<"); + } + } else { + + PGConnPtr admin_conn = createNewConnection(ConnType::ADMIN, with_ssl); + if (!executeQueries(admin_conn.get(), { "PROXYSQL FLUSH PGSQL QUERY CACHE" })) + return; + + for (const auto& test : tests) { + diag(">>>> Running %s - Shared Connection: %s <<<<", test.first.c_str(), diff_conn ? "False" : "True"); + + PGConnPtr admin_conn = createNewConnection(ConnType::ADMIN, with_ssl); + PGConnPtr backend_conn = createNewConnection(ConnType::BACKEND, with_ssl); + + if (!admin_conn || !backend_conn) { + fprintf(stderr, "Error: failed to connect to the database in file %s, line %d\n", __FILE__, __LINE__); + return; + } + + test.second(admin_conn.get(), backend_conn.get()); + diag(">>>> Done <<<<"); + } + } +} + +int main(int argc, char** argv) { + + plan(165*2); // Total number of tests planned + + if (cl.getEnv()) + return exit_status(); + + execute_tests(false, false); + execute_tests(false, true); + + return exit_status(); +}