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.
proxysql/test/tap/tests/unit/query_cache_unit-t.cpp

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();
}