feat(memdb×libmemdb): memdb 服务化落地——RPC 服务端 + 构建/测试/部署闭环
- libmemdb: 补全 flush() 惰性 TTL 驱逐;修复 explicit 误用/头文件缺失/ 时间戳碰撞误删;size()/get_db() 前 flush 消除脏读;三处双重哈希查找 合并;index_keys 去冗余字段;update(refresh_ttl) 改原地重索引, 免 remove+insert 的拷贝与重复 flush - memdb: 重写 RPC 服务端(libmsg RpcServer, :8011, 9 路由, mutex 保护); 补全 Makefile/_set.sh/_run.sh 与 --instance/--log-level CLI; SIGINT/SIGTERM 优雅退出;注册 services.yaml - 测试: libmemdb 单元测试 22 例 + memdb RPC 集成测试 12 场景(含并发) - 清理: 删除误合入的 db2/create_sqlite3.sql 旧版(scripts/ 下为权威版)
This commit is contained in:
1 parent
a18a7de1ca
commit
26de527a09
10 files changed
+1016
-116
No files matched your search
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* test_libmemdb.cpp — libmemdb 模板类单元测试
|
||||
*
|
||||
* 编译: g++ -std=c++20 -O0 -g -Wall -Werror -I ../libmemdb/inc -o test_libmemdb test_libmemdb.cpp
|
||||
* 运行: ./test_libmemdb
|
||||
*/
|
||||
|
||||
#include <us/memdb.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
static int g_passed = 0;
|
||||
static int g_failed = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { \
|
||||
std::cout << " " << (name) << " ... "; \
|
||||
} while (0)
|
||||
|
||||
#define PASS() \
|
||||
do { \
|
||||
std::cout << "PASS" << std::endl; \
|
||||
++g_passed; \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { \
|
||||
std::cout << "FAIL: " << (msg) << std::endl; \
|
||||
++g_failed; \
|
||||
} while (0)
|
||||
|
||||
#define CHECK(cond, msg) \
|
||||
do { \
|
||||
if (!(cond)) { FAIL(msg); return; } \
|
||||
} while (0)
|
||||
|
||||
// ── insert ──────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_insert_basic() {
|
||||
TEST("insert basic");
|
||||
Memdb<std::string, std::string> db;
|
||||
CHECK(db.insert("a", "1"), "insert new key should succeed");
|
||||
CHECK(db.size() == 1, "size should be 1");
|
||||
CHECK(db.search("a").has_value(), "search should find key");
|
||||
CHECK(db.search("a").value() == "1", "value should match");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_insert_duplicate() {
|
||||
TEST("insert duplicate");
|
||||
Memdb<std::string, std::string> db;
|
||||
db.insert("a", "1");
|
||||
CHECK(!db.insert("a", "2"), "insert duplicate should fail");
|
||||
CHECK(db.search("a").value() == "1", "value should be unchanged");
|
||||
CHECK(db.size() == 1, "size should still be 1");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_insert_int_keys() {
|
||||
TEST("insert int keys");
|
||||
Memdb<int, std::string> db;
|
||||
CHECK(db.insert(1, "one"), "");
|
||||
CHECK(db.insert(2, "two"), "");
|
||||
CHECK(db.search(1).value() == "one", "");
|
||||
CHECK(db.search(2).value() == "two", "");
|
||||
CHECK(db.size() == 2, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── remove ──────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_remove_existing() {
|
||||
TEST("remove existing");
|
||||
Memdb<std::string, std::string> db;
|
||||
db.insert("a", "1");
|
||||
db.insert("b", "2");
|
||||
CHECK(db.remove("a"), "remove existing should succeed");
|
||||
CHECK(!db.search("a").has_value(), "removed key should not be found");
|
||||
CHECK(db.size() == 1, "size should be 1");
|
||||
CHECK(db.search("b").value() == "2", "other key should still exist");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_remove_nonexistent() {
|
||||
TEST("remove nonexistent");
|
||||
Memdb<std::string, std::string> db;
|
||||
CHECK(!db.remove("ghost"), "remove nonexistent should fail");
|
||||
CHECK(db.size() == 0, "size should still be 0");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_remove_all() {
|
||||
TEST("remove all");
|
||||
Memdb<std::string, std::string> db;
|
||||
db.insert("a", "1");
|
||||
db.insert("b", "2");
|
||||
db.insert("c", "3");
|
||||
db.remove("a");
|
||||
db.remove("b");
|
||||
db.remove("c");
|
||||
CHECK(db.size() == 0, "size should be 0");
|
||||
CHECK(!db.search("a").has_value(), "");
|
||||
CHECK(!db.search("b").has_value(), "");
|
||||
CHECK(!db.search("c").has_value(), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── update ──────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_update_without_refresh() {
|
||||
TEST("update without refresh TTL");
|
||||
Memdb<std::string, std::string> db(1); // 1s TTL
|
||||
db.insert("a", "1");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
CHECK(db.update("a", "2", false), "update should succeed");
|
||||
CHECK(db.search("a").value() == "2", "value should be updated");
|
||||
// 500ms left — should still exist
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
||||
CHECK(db.search("a").has_value(), "should still exist (800ms < 1s)");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_update_with_refresh() {
|
||||
TEST("update with refresh TTL");
|
||||
Memdb<std::string, std::string> db(1); // 1s TTL
|
||||
db.insert("a", "1");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||
CHECK(db.update("a", "2", true), "update should succeed");
|
||||
CHECK(db.search("a").value() == "2", "value should be updated");
|
||||
// TTL was refreshed — should still exist
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
CHECK(db.search("a").has_value(), "should still exist after refresh");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_update_nonexistent() {
|
||||
TEST("update nonexistent");
|
||||
Memdb<std::string, std::string> db;
|
||||
CHECK(!db.update("ghost", "1"), "update nonexistent should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── search ──────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_search_nonexistent() {
|
||||
TEST("search nonexistent");
|
||||
Memdb<std::string, std::string> db;
|
||||
CHECK(!db.search("ghost").has_value(), "search nonexistent should return nullopt");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── size ────────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_size_empty() {
|
||||
TEST("size empty");
|
||||
Memdb<std::string, std::string> db;
|
||||
CHECK(db.size() == 0, "empty db should have size 0");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_size_after_ops() {
|
||||
TEST("size after operations");
|
||||
Memdb<std::string, std::string> db;
|
||||
db.insert("a", "1");
|
||||
db.insert("b", "2");
|
||||
CHECK(db.size() == 2, "size after 2 inserts");
|
||||
db.remove("a");
|
||||
CHECK(db.size() == 1, "size after remove");
|
||||
db.insert("c", "3");
|
||||
CHECK(db.size() == 2, "size after re-insert");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── TTL expiration (flush) ──────────────────────────────────────────────
|
||||
|
||||
static void test_ttl_expiration() {
|
||||
TEST("TTL expiration");
|
||||
Memdb<std::string, std::string> db(1); // 1s TTL
|
||||
db.insert("a", "1");
|
||||
db.insert("b", "2");
|
||||
CHECK(db.size() == 2, "initial size 2");
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
|
||||
|
||||
// search triggers flush
|
||||
CHECK(!db.search("a").has_value(), "a should be expired");
|
||||
CHECK(!db.search("b").has_value(), "b should be expired");
|
||||
CHECK(db.size() == 0, "size should be 0 after flush");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_ttl_partial_expiration() {
|
||||
TEST("TTL partial expiration");
|
||||
Memdb<std::string, std::string> db(1);
|
||||
db.insert("a", "1");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(600));
|
||||
db.insert("b", "2"); // fresher
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
// a: 1100ms old → expired, b: 500ms old → alive
|
||||
CHECK(!db.search("a").has_value(), "a should be expired");
|
||||
CHECK(db.search("b").has_value(), "b should still be alive");
|
||||
CHECK(db.size() == 1, "size should be 1");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_ttl_zero_means_immediate() {
|
||||
TEST("TTL zero means immediate expiration");
|
||||
Memdb<std::string, std::string> db(0);
|
||||
db.insert("a", "1");
|
||||
CHECK(!db.search("a").has_value(), "zero TTL should expire immediately");
|
||||
CHECK(db.size() == 0, "size should be 0");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── get_db / show ───────────────────────────────────────────────────────
|
||||
|
||||
static void test_get_db() {
|
||||
TEST("get_db returns all entries");
|
||||
Memdb<std::string, std::string> db(10);
|
||||
db.insert("x", "100");
|
||||
db.insert("y", "200");
|
||||
const auto& lst = db.get_db();
|
||||
CHECK(lst.size() == 2, "list should have 2 entries");
|
||||
// verify values are accessible
|
||||
bool found_x = false, found_y = false;
|
||||
for (const auto& col : lst) {
|
||||
if (col.key == "x" && col.value == "100") found_x = true;
|
||||
if (col.key == "y" && col.value == "200") found_y = true;
|
||||
}
|
||||
CHECK(found_x && found_y, "both entries should be in the list");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── get_keys ────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_get_keys() {
|
||||
TEST("get_keys returns key index");
|
||||
Memdb<std::string, std::string> db;
|
||||
db.insert("k1", "v1");
|
||||
const auto& keys = db.get_keys();
|
||||
CHECK(keys.size() == 1, "keys should have 1 entry");
|
||||
CHECK(keys.find("k1") != keys.end(), "k1 should be in keys");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── get_time_touch ──────────────────────────────────────────────────────
|
||||
|
||||
static void test_get_time_touch() {
|
||||
TEST("get_time_touch returns sorted timestamps");
|
||||
Memdb<std::string, std::string> db(10);
|
||||
db.insert("a", "1");
|
||||
db.insert("b", "2");
|
||||
const auto& tt = db.get_time_touch();
|
||||
CHECK(tt.size() == 2, "time_touch should have 2 entries");
|
||||
// verify sorted order (a before b since a was inserted first)
|
||||
auto it = tt.begin();
|
||||
auto first = it;
|
||||
++it;
|
||||
CHECK(first->item->timestamp <= it->item->timestamp,
|
||||
"time_touch should be sorted by timestamp");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── copy assignment ─────────────────────────────────────────────────────
|
||||
|
||||
static void test_copy_assignment() {
|
||||
TEST("copy assignment");
|
||||
Memdb<std::string, std::string> db1;
|
||||
db1.insert("a", "1");
|
||||
Memdb<std::string, std::string> db2;
|
||||
db2 = db1;
|
||||
CHECK(db2.search("a").value() == "1", "copied db should have same data");
|
||||
// verify independence
|
||||
db2.insert("b", "2");
|
||||
CHECK(db1.size() == 1, "original should be unchanged");
|
||||
CHECK(db2.size() == 2, "copy should be independent");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ── edge cases ──────────────────────────────────────────────────────────
|
||||
|
||||
static void test_large_number_of_keys() {
|
||||
TEST("large number of keys");
|
||||
Memdb<int, int> db(60);
|
||||
const int N = 1000;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
CHECK(db.insert(i, i * 10), "all inserts should succeed");
|
||||
}
|
||||
CHECK(db.size() == static_cast<size_t>(N), "size should be N");
|
||||
for (int i = 0; i < N; ++i) {
|
||||
CHECK(db.search(i).value() == i * 10, "all values should match");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_reinsert_after_remove() {
|
||||
TEST("reinsert after remove");
|
||||
Memdb<std::string, std::string> db;
|
||||
db.insert("a", "1");
|
||||
db.remove("a");
|
||||
CHECK(db.insert("a", "2"), "reinsert should succeed");
|
||||
CHECK(db.search("a").value() == "2", "value should be updated");
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_update_without_refresh_keeps_old_ttl() {
|
||||
TEST("update without refresh keeps old TTL");
|
||||
Memdb<std::string, std::string> db(1);
|
||||
db.insert("a", "1");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||
db.update("a", "2", false); // value changes, TTL unchanged
|
||||
// wait remaining 200ms+ — should expire
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
||||
CHECK(!db.search("a").has_value(), "should expire at original TTL");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
std::cout << "=== libmemdb unit tests ===" << std::endl;
|
||||
|
||||
test_insert_basic();
|
||||
test_insert_duplicate();
|
||||
test_insert_int_keys();
|
||||
test_remove_existing();
|
||||
test_remove_nonexistent();
|
||||
test_remove_all();
|
||||
test_update_without_refresh();
|
||||
test_update_with_refresh();
|
||||
test_update_nonexistent();
|
||||
test_search_nonexistent();
|
||||
test_size_empty();
|
||||
test_size_after_ops();
|
||||
test_ttl_expiration();
|
||||
test_ttl_partial_expiration();
|
||||
test_ttl_zero_means_immediate();
|
||||
test_get_db();
|
||||
test_get_keys();
|
||||
test_get_time_touch();
|
||||
test_copy_assignment();
|
||||
test_large_number_of_keys();
|
||||
test_reinsert_after_remove();
|
||||
test_update_without_refresh_keeps_old_ttl();
|
||||
|
||||
std::cout << "\n=== Results: " << g_passed << " passed, "
|
||||
<< g_failed << " failed ===" << std::endl;
|
||||
return g_failed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user