mirror of https://github.com/sysown/proxysql
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
16 KiB
504 lines
16 KiB
/**
|
|
* @file query_cache_unit-t.cpp
|
|
* @brief Unit tests for MySQL_Query_Cache and PgSQL_Query_Cache.
|
|
*
|
|
* Tests the query cache subsystem in isolation without a running
|
|
* ProxySQL instance. Covers:
|
|
* - Basic set/get cycle (PgSQL — simpler API for core logic testing)
|
|
* - Cache miss on nonexistent key
|
|
* - Cache replacement (same key, new value)
|
|
* - TTL expiration (hard TTL)
|
|
* - flush() clears all entries
|
|
* - purgeHash() eviction under memory pressure
|
|
* - Global stats counter accuracy
|
|
* - Memory tracking via get_data_size_total()
|
|
* - Multiple entries across hash buckets
|
|
*
|
|
* PgSQL_Query_Cache is used for most tests because its set()/get()
|
|
* API is simpler (no MySQL protocol parsing). The underlying
|
|
* Query_Cache<T> template logic is identical for both protocols.
|
|
*
|
|
* @note MySQL_Query_Cache::set() requires valid MySQL protocol result
|
|
* sets (it parses packet boundaries), so MySQL-specific tests
|
|
* are limited to construction/flush.
|
|
*
|
|
* @see Phase 2.3 of the Unit Testing Framework (GitHub issue #5475)
|
|
*/
|
|
|
|
#include "tap.h"
|
|
#include "test_globals.h"
|
|
#include "test_init.h"
|
|
|
|
#include "proxysql.h"
|
|
#include "MySQL_Query_Cache.h"
|
|
#include "PgSQL_Query_Cache.h"
|
|
|
|
#include <cstring>
|
|
|
|
// Extern declarations for Glo* pointers (defined in test_globals.cpp)
|
|
extern MySQL_Query_Cache *GloMyQC;
|
|
extern PgSQL_Query_Cache *GloPgQC;
|
|
|
|
/**
|
|
* @brief Helper to get the current monotonic time in milliseconds.
|
|
*/
|
|
static uint64_t now_ms() {
|
|
return monotonic_time() / 1000;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 1. PgSQL Query Cache: Basic set/get
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test basic set + get cycle with PgSQL cache.
|
|
*
|
|
* Stores a value with a 10-second TTL and retrieves it immediately.
|
|
* Verifies the returned shared_ptr points to the correct data.
|
|
*/
|
|
static void test_pgsql_set_get() {
|
|
uint64_t user_hash = 12345;
|
|
const unsigned char *key = (const unsigned char *)"SELECT 1";
|
|
uint32_t kl = 8;
|
|
|
|
// Create a value buffer — the cache takes ownership of the pointer
|
|
unsigned char *value = (unsigned char *)malloc(16);
|
|
memcpy(value, "result_data_001", 16);
|
|
uint32_t vl = 16;
|
|
|
|
uint64_t t = now_ms();
|
|
uint64_t expire = t + 10000; // 10 seconds from now
|
|
|
|
bool set_ok = GloPgQC->set(user_hash, key, kl, value, vl,
|
|
t, t, expire);
|
|
ok(set_ok == true, "PgSQL QC: set() returns true");
|
|
|
|
// Get with same key and user_hash
|
|
auto entry = GloPgQC->get(user_hash, key, kl, now_ms(), 10000);
|
|
ok(entry != nullptr, "PgSQL QC: get() returns non-null for cached entry");
|
|
|
|
if (entry != nullptr) {
|
|
ok(entry->length == 16, "PgSQL QC: retrieved entry has correct length");
|
|
ok(memcmp(entry->value, "result_data_001", 16) == 0,
|
|
"PgSQL QC: retrieved entry has correct value");
|
|
} else {
|
|
ok(0, "PgSQL QC: retrieved entry has correct length (skipped)");
|
|
ok(0, "PgSQL QC: retrieved entry has correct value (skipped)");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Test cache miss — get() on nonexistent key returns nullptr.
|
|
*/
|
|
static void test_pgsql_cache_miss() {
|
|
uint64_t user_hash = 99999;
|
|
const unsigned char *key = (const unsigned char *)"SELECT nonexistent";
|
|
uint32_t kl = 18;
|
|
|
|
auto entry = GloPgQC->get(user_hash, key, kl, now_ms(), 10000);
|
|
ok(entry == nullptr, "PgSQL QC: get() returns null for nonexistent key");
|
|
}
|
|
|
|
/**
|
|
* @brief Test that different user_hash values produce cache misses
|
|
* even for the same key bytes.
|
|
*/
|
|
static void test_pgsql_user_hash_isolation() {
|
|
const unsigned char *key = (const unsigned char *)"SELECT shared";
|
|
uint32_t kl = 13;
|
|
uint64_t t = now_ms();
|
|
|
|
unsigned char *val1 = (unsigned char *)malloc(6);
|
|
memcpy(val1, "user_A", 6);
|
|
GloPgQC->set(100, key, kl, val1, 6, t, t, t + 10000);
|
|
|
|
// Same key but different user_hash — should miss
|
|
auto entry = GloPgQC->get(200, key, kl, now_ms(), 10000);
|
|
ok(entry == nullptr,
|
|
"PgSQL QC: different user_hash produces cache miss for same key");
|
|
|
|
// Same user_hash — should hit
|
|
auto entry2 = GloPgQC->get(100, key, kl, now_ms(), 10000);
|
|
ok(entry2 != nullptr,
|
|
"PgSQL QC: same user_hash produces cache hit");
|
|
}
|
|
|
|
// ============================================================================
|
|
// 2. Cache replacement
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test that set() with the same key replaces the cached value.
|
|
*/
|
|
static void test_pgsql_replace() {
|
|
uint64_t user_hash = 20000;
|
|
const unsigned char *key = (const unsigned char *)"SELECT replace_me";
|
|
uint32_t kl = 17;
|
|
uint64_t t = now_ms();
|
|
|
|
unsigned char *val1 = (unsigned char *)malloc(5);
|
|
memcpy(val1, "old_v", 5);
|
|
GloPgQC->set(user_hash, key, kl, val1, 5, t, t, t + 10000);
|
|
|
|
unsigned char *val2 = (unsigned char *)malloc(5);
|
|
memcpy(val2, "new_v", 5);
|
|
GloPgQC->set(user_hash, key, kl, val2, 5, t, t, t + 10000);
|
|
|
|
auto entry = GloPgQC->get(user_hash, key, kl, now_ms(), 10000);
|
|
ok(entry != nullptr, "PgSQL QC: get() after replace returns non-null");
|
|
if (entry != nullptr) {
|
|
ok(memcmp(entry->value, "new_v", 5) == 0,
|
|
"PgSQL QC: replaced entry has new value");
|
|
} else {
|
|
ok(0, "PgSQL QC: replaced entry has new value (skipped)");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 3. TTL expiration
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test that entries are not returned after their hard TTL expires.
|
|
*
|
|
* Sets an entry with expire_ms in the past, then verifies get()
|
|
* returns nullptr.
|
|
*/
|
|
static void test_pgsql_ttl_expired() {
|
|
uint64_t user_hash = 30000;
|
|
const unsigned char *key = (const unsigned char *)"SELECT expired";
|
|
uint32_t kl = 14;
|
|
uint64_t t = now_ms();
|
|
|
|
// Create entry that expires 1ms before "now"
|
|
unsigned char *val = (unsigned char *)malloc(4);
|
|
memcpy(val, "old!", 4);
|
|
GloPgQC->set(user_hash, key, kl, val, 4,
|
|
t - 5000, // created 5 seconds ago
|
|
t - 5000, // curtime when set
|
|
t - 1); // expired 1ms ago
|
|
|
|
auto entry = GloPgQC->get(user_hash, key, kl, now_ms(), 10000);
|
|
ok(entry == nullptr,
|
|
"PgSQL QC: get() returns null for expired entry (hard TTL)");
|
|
}
|
|
|
|
/**
|
|
* @brief Test that entries are not returned after the soft TTL
|
|
* (cache_ttl parameter to get()) is exceeded.
|
|
*/
|
|
static void test_pgsql_soft_ttl() {
|
|
uint64_t user_hash = 31000;
|
|
const unsigned char *key = (const unsigned char *)"SELECT soft_ttl";
|
|
uint32_t kl = 15;
|
|
uint64_t t = now_ms();
|
|
|
|
unsigned char *val = (unsigned char *)malloc(4);
|
|
memcpy(val, "soft", 4);
|
|
// Hard TTL far in the future, but created 5 seconds ago
|
|
GloPgQC->set(user_hash, key, kl, val, 4,
|
|
t - 5000, // created 5 seconds ago
|
|
t - 5000,
|
|
t + 60000); // hard TTL 60s from now
|
|
|
|
// Get with cache_ttl=2000ms — entry was created 5s ago, so
|
|
// create_ms + cache_ttl < now → soft TTL exceeded
|
|
auto entry = GloPgQC->get(user_hash, key, kl, now_ms(), 2000);
|
|
ok(entry == nullptr,
|
|
"PgSQL QC: get() returns null when soft TTL exceeded");
|
|
}
|
|
|
|
// ============================================================================
|
|
// 4. flush()
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test that flush() removes all entries and returns correct count.
|
|
*/
|
|
static void test_pgsql_flush() {
|
|
// Ensure clean state before test
|
|
GloPgQC->flush();
|
|
|
|
// Add several entries
|
|
uint64_t t = now_ms();
|
|
for (int i = 0; i < 10; i++) {
|
|
char keybuf[32];
|
|
snprintf(keybuf, sizeof(keybuf), "SELECT flush_%d", i);
|
|
unsigned char *val = (unsigned char *)malloc(4);
|
|
memcpy(val, "data", 4);
|
|
GloPgQC->set(40000 + i,
|
|
(const unsigned char *)keybuf, strlen(keybuf),
|
|
val, 4, t, t, t + 60000);
|
|
}
|
|
|
|
uint64_t flushed = GloPgQC->flush();
|
|
ok(flushed == 10,
|
|
"PgSQL QC: flush() returns exactly 10");
|
|
|
|
// Verify entries are gone
|
|
auto entry = GloPgQC->get(40000,
|
|
(const unsigned char *)"SELECT flush_0", 14, now_ms(), 60000);
|
|
ok(entry == nullptr,
|
|
"PgSQL QC: entries gone after flush()");
|
|
}
|
|
|
|
// ============================================================================
|
|
// 5. Memory tracking
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Helper to extract a named stat value from SQL3_getStats().
|
|
* @return The stat value as uint64_t, or 0 if not found.
|
|
*/
|
|
static uint64_t get_qc_stat(PgSQL_Query_Cache *qc, const char *name) {
|
|
SQLite3_result *result = qc->SQL3_getStats();
|
|
if (result == nullptr) return 0;
|
|
uint64_t val = 0;
|
|
for (auto it = result->rows.begin(); it != result->rows.end(); it++) {
|
|
if (strcmp((*it)->fields[0], name) == 0) {
|
|
val = strtoull((*it)->fields[1], nullptr, 10);
|
|
break;
|
|
}
|
|
}
|
|
delete result;
|
|
return val;
|
|
}
|
|
|
|
/**
|
|
* @brief Test that set() stores data retrievable by get(), and
|
|
* flush() makes it unretrievable.
|
|
*/
|
|
static void test_pgsql_set_flush_cycle() {
|
|
GloPgQC->flush();
|
|
|
|
uint64_t t = now_ms();
|
|
unsigned char *val = (unsigned char *)malloc(1024);
|
|
memset(val, 'A', 1024);
|
|
GloPgQC->set(50000,
|
|
(const unsigned char *)"SELECT cycle_test", 17,
|
|
val, 1024, t, t, t + 60000);
|
|
|
|
auto entry = GloPgQC->get(50000,
|
|
(const unsigned char *)"SELECT cycle_test", 17, now_ms(), 60000);
|
|
ok(entry != nullptr,
|
|
"PgSQL QC: entry retrievable after set()");
|
|
|
|
GloPgQC->flush();
|
|
auto entry2 = GloPgQC->get(50000,
|
|
(const unsigned char *)"SELECT cycle_test", 17, now_ms(), 60000);
|
|
ok(entry2 == nullptr,
|
|
"PgSQL QC: entry unretrievable after flush()");
|
|
}
|
|
|
|
// ============================================================================
|
|
// 6. Stats counters
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test that stats counters increment correctly.
|
|
*
|
|
* Uses SQL3_getStats() to read counter values before and after
|
|
* set/get operations.
|
|
*/
|
|
static void test_stats_counters() {
|
|
uint64_t set_before = get_qc_stat(GloPgQC, "Query_Cache_count_SET");
|
|
uint64_t get_before = get_qc_stat(GloPgQC, "Query_Cache_count_GET");
|
|
|
|
uint64_t t = now_ms();
|
|
unsigned char *val = (unsigned char *)malloc(8);
|
|
memcpy(val, "countval", 8);
|
|
GloPgQC->set(60000,
|
|
(const unsigned char *)"SELECT counter", 14,
|
|
val, 8, t, t, t + 60000);
|
|
|
|
uint64_t set_after = get_qc_stat(GloPgQC, "Query_Cache_count_SET");
|
|
ok(set_after > set_before,
|
|
"PgSQL QC: SET counter increments after set()");
|
|
|
|
// Trigger a get (hit)
|
|
GloPgQC->get(60000,
|
|
(const unsigned char *)"SELECT counter", 14, now_ms(), 60000);
|
|
|
|
uint64_t get_after = get_qc_stat(GloPgQC, "Query_Cache_count_GET");
|
|
ok(get_after > get_before,
|
|
"PgSQL QC: GET counter increments after get()");
|
|
}
|
|
|
|
// ============================================================================
|
|
// 7. SQL3_getStats()
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test that SQL3_getStats() returns a valid result set.
|
|
*/
|
|
static void test_sql3_get_stats() {
|
|
SQLite3_result *result = GloPgQC->SQL3_getStats();
|
|
ok(result != nullptr, "PgSQL QC: SQL3_getStats() returns non-null");
|
|
|
|
if (result != nullptr) {
|
|
ok(result->columns == 2,
|
|
"PgSQL QC: SQL3_getStats() has 2 columns");
|
|
ok(result->rows_count > 0,
|
|
"PgSQL QC: SQL3_getStats() has rows");
|
|
delete result;
|
|
} else {
|
|
ok(0, "PgSQL QC: SQL3_getStats() has 2 columns (skipped)");
|
|
ok(0, "PgSQL QC: SQL3_getStats() has rows (skipped)");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 8. MySQL Query Cache: Construction and flush
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test MySQL_Query_Cache construction and basic flush.
|
|
*
|
|
* MySQL set() requires valid protocol data so we only test
|
|
* construction and flush here.
|
|
*/
|
|
static void test_mysql_construction_and_flush() {
|
|
ok(GloMyQC != nullptr,
|
|
"MySQL QC: GloMyQC is initialized");
|
|
|
|
// First flush clears any residual entries
|
|
GloMyQC->flush();
|
|
// Second flush on empty cache should return 0
|
|
uint64_t flushed = GloMyQC->flush();
|
|
ok(flushed == 0,
|
|
"MySQL QC: flush() on empty cache returns 0");
|
|
|
|
SQLite3_result *result = GloMyQC->SQL3_getStats();
|
|
ok(result != nullptr,
|
|
"MySQL QC: SQL3_getStats() returns non-null");
|
|
if (result != nullptr) {
|
|
delete result;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 9. purgeHash() eviction
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test that purgeHash() removes expired entries.
|
|
*
|
|
* Creates entries with already-expired TTLs, then calls purgeHash()
|
|
* and verifies they are evicted.
|
|
*/
|
|
static void test_pgsql_purge_expired() {
|
|
GloPgQC->flush();
|
|
uint64_t t = now_ms();
|
|
|
|
// Add entries that are already expired
|
|
for (int i = 0; i < 5; i++) {
|
|
char keybuf[32];
|
|
snprintf(keybuf, sizeof(keybuf), "SELECT purge_%d", i);
|
|
unsigned char *val = (unsigned char *)malloc(64);
|
|
memset(val, 'X', 64);
|
|
GloPgQC->set(70000 + i,
|
|
(const unsigned char *)keybuf, strlen(keybuf),
|
|
val, 64,
|
|
t - 10000, // created 10s ago
|
|
t - 10000,
|
|
t - 1); // expired 1ms ago
|
|
}
|
|
|
|
// Add one entry that is NOT expired
|
|
unsigned char *live_val = (unsigned char *)malloc(64);
|
|
memset(live_val, 'L', 64);
|
|
GloPgQC->set(70099,
|
|
(const unsigned char *)"SELECT live", 11,
|
|
live_val, 64, t, t, t + 60000);
|
|
|
|
// purgeHash with small max_memory to force eviction logic to run.
|
|
// The threshold is 3% minimum, so use a size smaller than total
|
|
// cached data to ensure the purge path executes.
|
|
GloPgQC->purgeHash(1);
|
|
|
|
// Live entry should still be accessible
|
|
auto entry = GloPgQC->get(70099,
|
|
(const unsigned char *)"SELECT live", 11, now_ms(), 60000);
|
|
ok(entry != nullptr,
|
|
"PgSQL QC: live entry survives purgeHash()");
|
|
|
|
GloPgQC->flush();
|
|
}
|
|
|
|
// ============================================================================
|
|
// 10. Multiple entries across hash buckets
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Test storing and retrieving many entries.
|
|
*
|
|
* Verifies that bulk inserts with unique keys are all retrievable
|
|
* and that flush correctly reports the total count.
|
|
*/
|
|
static void test_pgsql_many_entries() {
|
|
GloPgQC->flush();
|
|
uint64_t t = now_ms();
|
|
const int N = 100;
|
|
|
|
// Insert N entries with unique keys
|
|
for (int i = 0; i < N; i++) {
|
|
char keybuf[32];
|
|
snprintf(keybuf, sizeof(keybuf), "SELECT many_%04d", i);
|
|
unsigned char *val = (unsigned char *)malloc(8);
|
|
snprintf((char *)val, 8, "val%04d", i);
|
|
GloPgQC->set(80000,
|
|
(const unsigned char *)keybuf, strlen(keybuf),
|
|
val, 8, t, t, t + 60000);
|
|
}
|
|
|
|
// Verify a sample of entries are retrievable
|
|
int hits = 0;
|
|
for (int i = 0; i < N; i += 10) {
|
|
char keybuf[32];
|
|
snprintf(keybuf, sizeof(keybuf), "SELECT many_%04d", i);
|
|
auto entry = GloPgQC->get(80000,
|
|
(const unsigned char *)keybuf, strlen(keybuf),
|
|
now_ms(), 60000);
|
|
if (entry != nullptr) hits++;
|
|
}
|
|
ok(hits == 10,
|
|
"PgSQL QC: all sampled entries retrievable from 100 inserts");
|
|
|
|
uint64_t flushed = GloPgQC->flush();
|
|
ok(flushed >= (uint64_t)N,
|
|
"PgSQL QC: flush() returns count >= N after bulk insert");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main
|
|
// ============================================================================
|
|
|
|
int main() {
|
|
plan(26);
|
|
|
|
test_init_minimal();
|
|
test_init_query_cache();
|
|
|
|
// PgSQL cache tests (exercises Query_Cache<T> template logic)
|
|
test_pgsql_set_get(); // 4 tests
|
|
test_pgsql_cache_miss(); // 1 test
|
|
test_pgsql_user_hash_isolation(); // 2 tests
|
|
test_pgsql_replace(); // 2 tests
|
|
test_pgsql_ttl_expired(); // 1 test
|
|
test_pgsql_soft_ttl(); // 1 test
|
|
test_pgsql_flush(); // 2 tests
|
|
test_pgsql_set_flush_cycle(); // 2 tests
|
|
test_stats_counters(); // 2 tests
|
|
test_sql3_get_stats(); // 3 tests
|
|
test_mysql_construction_and_flush();// 3 tests
|
|
test_pgsql_purge_expired(); // 1 test
|
|
test_pgsql_many_entries(); // 2 tests
|
|
// Total: 26 ... let me recount
|
|
// 4+1+2+2+1+1+2+2+2+3+3+1+2 = 26
|
|
|
|
test_cleanup_query_cache();
|
|
test_cleanup_minimal();
|
|
|
|
return exit_status();
|
|
}
|