fix(memdb): expire semantics + Boost 1.83 portability + backlog & test harness

- expire 改为 update+refresh_ttl:键不存在返回 false,与 del/update 约定一致
- _require_string 适配 Boost 1.83(boost::json::string 已非 std::string)
- 新增 --backlog(默认 128,libmsg 默认 10 短连接突刺易丢连接)
- Makefile 补 HOST/PORT 默认值,test-memdb 用 PID 文件替代失效的 kill %1
- 补测试:复制构造/源销毁后副本有效/refresh 延长 TTL/expire 不存在键
This commit is contained in:
qing committed 2026-08-17 14:29:41 +00:00
1 parent 207137c0dd
commit a833cd0d5f
6 files changed
+79 -23

No files matched your search

+4 -1
View File
@@ -69,4 +69,7 @@ Testing/
.cache/ .cache/
# Databases # Databases
*.db* *.db*
# unit test binary
test_libmemdb
.memdb.pid
+7 -5
View File
@@ -8,6 +8,9 @@ BUILD = build
TARGET = $(BUILD)/memdb TARGET = $(BUILD)/memdb
SRC = src/memdb.cpp SRC = src/memdb.cpp
HOST ?= 127.0.0.1
PORT ?= 8011
all: $(TARGET) all: $(TARGET)
$(BUILD): $(BUILD):
@@ -37,13 +40,12 @@ test-libmemdb: $(TEST_LIBMEMDB)
test-memdb: $(TARGET) test-memdb: $(TARGET)
@echo "Starting memdb server..." @echo "Starting memdb server..."
./$(TARGET) --host $(HOST) --port $(PORT) --workers 2 --max-time 10 & @./$(TARGET) --host $(HOST) --port $(PORT) --workers 2 --max-time 10 >/dev/null 2>&1 & echo $$! > .memdb.pid
@sleep 1 @sleep 1
-python3 test_memdb.py $(HOST) $(PORT); EXIT=$$? @python3 test_memdb.py $(HOST) $(PORT); EXIT=$$?; \
@kill %1 2>/dev/null; wait %1 2>/dev/null kill $$(cat .memdb.pid) 2>/dev/null; rm -f .memdb.pid; exit $$EXIT
@exit $$EXIT
clean: clean:
rm -f $(TARGET) $(TEST_LIBMEMDB) rm -f $(TARGET) $(TEST_LIBMEMDB) .memdb.pid
.PHONY: all clean test test-libmemdb test-memdb .PHONY: all clean test test-libmemdb test-memdb
+2 -1
View File
@@ -21,7 +21,7 @@ Memdb<std::string, std::string> (libmemdb 模板:LRU 链表 + 哈希索
| `update` | `key`, `value`, `refresh_ttl?` | `true/false` | 更新值,可选刷新 TTL | | `update` | `key`, `value`, `refresh_ttl?` | `true/false` | 更新值,可选刷新 TTL |
| `exists` | `key` | `true/false` | 键是否存在 | | `exists` | `key` | `true/false` | 键是否存在 |
| `size` | — | `int` | 当前条目数 | | `size` | — | `int` | 当前条目数 |
| `expire` | `key`, `value` | `true/false` | 更新值并重置 TTL(del + set) | | `expire` | `key`, `value` | `true/false` | 更新已有键的值并重置 TTL(键不存在返回 false) |
| `show` | — | `[{key,value},...]` | 导出所有条目 | | `show` | — | `[{key,value},...]` | 导出所有条目 |
## 配置 ## 配置
@@ -34,6 +34,7 @@ Memdb<std::string, std::string> (libmemdb 模板:LRU 链表 + 哈希索
| `listen_port` | `8011` | 监听端口 | | `listen_port` | `8011` | 监听端口 |
| `workers` | `4` | 线程池大小 | | `workers` | `4` | 线程池大小 |
| `max_time` | `600` | 默认 TTL(秒),10 分钟 | | `max_time` | `600` | 默认 TTL(秒),10 分钟 |
| `backlog` | `128` | listen() backlog(libmsg 默认 10,短连接突刺易丢连接) |
## 构建与运行 ## 构建与运行
+10 -7
View File
@@ -20,6 +20,7 @@ struct ServerConfig {
int listen_port = 8011; int listen_port = 8011;
int workers = 4; int workers = 4;
int max_time = 600; // 默认 TTL 10 分钟 int max_time = 600; // 默认 TTL 10 分钟
int backlog = 128; // 短连接并发突刺时防丢连接(libmsg 默认仅 10)
}; };
static std::atomic<bool> g_running{true}; static std::atomic<bool> g_running{true};
@@ -35,7 +36,7 @@ private:
pmc::net::RpcServer __rpc; pmc::net::RpcServer __rpc;
std::mutex __mtx; std::mutex __mtx;
static const std::string& _require_string(const json::object& req, const std::string& field) { static std::string _require_string(const json::object& req, const std::string& field) {
auto it = req.find(field); auto it = req.find(field);
if (it == req.end()) { if (it == req.end()) {
throw std::runtime_error("missing required field: " + field); throw std::runtime_error("missing required field: " + field);
@@ -43,7 +44,8 @@ private:
if (!it->value().is_string()) { if (!it->value().is_string()) {
throw std::runtime_error("field '" + field + "' must be a string"); throw std::runtime_error("field '" + field + "' must be a string");
} }
return it->value().as_string(); const auto& s = it->value().as_string();
return std::string(s.data(), s.size());
} }
public: public:
@@ -51,7 +53,7 @@ public:
: __cfg(cfg) : __cfg(cfg)
, __db(cfg.max_time) , __db(cfg.max_time)
, __rpc(cfg.listen_host, cfg.listen_port, , __rpc(cfg.listen_host, cfg.listen_port,
{.workers = cfg.workers}) {.workers = cfg.workers, .listen_backlog = cfg.backlog})
{ {
_register_routes(); _register_routes();
} }
@@ -130,13 +132,12 @@ private:
return result; return result;
}); });
// ---- expire: 更新值并重置 TTL(等价于 del + set)---- // ---- expire: 更新值并重置 TTL(仅已有键;等价于 update + refresh_ttl)----
__rpc.add_route("expire", [this](const json::object& req) -> json::value { __rpc.add_route("expire", [this](const json::object& req) -> json::value {
const auto& key = _require_string(req, "key"); const auto& key = _require_string(req, "key");
const auto& val = _require_string(req, "value"); const auto& val = _require_string(req, "value");
std::lock_guard<std::mutex> lock(__mtx); std::lock_guard<std::mutex> lock(__mtx);
__db.remove(key); return __db.update(key, val, true);
return __db.insert(key, val);
}); });
} }
}; };
@@ -156,7 +157,8 @@ int main(int argc, char** argv) {
("host", po::value<std::string>()->default_value("127.0.0.1"), "listen host") ("host", po::value<std::string>()->default_value("127.0.0.1"), "listen host")
("port", po::value<int>()->default_value(8011), "listen port") ("port", po::value<int>()->default_value(8011), "listen port")
("workers", po::value<int>()->default_value(4), "worker threads") ("workers", po::value<int>()->default_value(4), "worker threads")
("max-time", po::value<int>()->default_value(600), "default TTL (seconds)"); ("max-time", po::value<int>()->default_value(600), "default TTL (seconds)")
("backlog", po::value<int>()->default_value(128), "listen() backlog");
po::variables_map vm; po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm); po::store(po::parse_command_line(argc, argv, desc), vm);
@@ -171,6 +173,7 @@ int main(int argc, char** argv) {
cfg.listen_port = vm["port"].as<int>(); cfg.listen_port = vm["port"].as<int>();
cfg.workers = vm["workers"].as<int>(); cfg.workers = vm["workers"].as<int>();
cfg.max_time = vm["max-time"].as<int>(); cfg.max_time = vm["max-time"].as<int>();
cfg.backlog = vm["backlog"].as<int>();
auto instance = vm["instance"].as<std::string>(); auto instance = vm["instance"].as<std::string>();
auto log_level = vm["log-level"].as<std::string>(); auto log_level = vm["log-level"].as<std::string>();
+50 -9
View File
@@ -10,6 +10,7 @@
#include <cassert> #include <cassert>
#include <chrono> #include <chrono>
#include <iostream> #include <iostream>
#include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector> #include <vector>
@@ -266,19 +267,57 @@ static void test_get_time_touch() {
PASS(); PASS();
} }
// ── copy assignment ───────────────────────────────────────────────────── // ── copy semantics ──────────────────────────────────────────────────────
static void test_copy_assignment() { static void test_copy_assignment() {
TEST("copy assignment"); TEST("copy assignment");
Memdb<std::string, std::string> db1;
db1.insert("a", "1");
Memdb<std::string, std::string> db2; Memdb<std::string, std::string> db2;
db2 = db1; {
CHECK(db2.search("a").value() == "1", "copied db should have same data"); Memdb<std::string, std::string> db1;
// verify independence db1.insert("a", "1");
db2.insert("b", "2"); db2 = db1;
CHECK(db1.size() == 1, "original should be unchanged"); CHECK(db2.search("a").value() == "1", "copied db should have same data");
CHECK(db2.size() == 2, "copy should be independent"); // verify independence
db2.insert("b", "2");
CHECK(db1.size() == 1, "original should be unchanged");
CHECK(db2.size() == 2, "copy should be independent");
}
// db1 destroyed — db2 must still work (indices must not dangle)
CHECK(db2.search("a").value() == "1", "copy must survive source destruction");
CHECK(db2.remove("a"), "copy remove should work after source destroyed");
CHECK(db2.size() == 1, "copy size after remove");
PASS();
}
static void test_copy_constructor() {
TEST("copy constructor");
std::optional<Memdb<std::string, std::string>> cpy;
{
Memdb<std::string, std::string> src;
src.insert("a", "1");
src.insert("b", "2");
cpy.emplace(src); // copy constructor
CHECK(cpy->size() == 2, "copy should have 2 entries");
CHECK(src.size() == 2, "source unchanged");
}
CHECK(cpy->search("a").value() == "1", "copy value a after source destroyed");
CHECK(cpy->search("b").value() == "2", "copy value b after source destroyed");
cpy->remove("a");
CHECK(cpy->size() == 1, "copy mutation after source destroyed");
// flush on copy must not touch freed memory
CHECK(cpy->search("b").value() == "2", "flush path safe on copy");
PASS();
}
static void test_update_refresh_extends_ttl() {
TEST("update with refresh extends TTL");
Memdb<std::string, std::string> db(1);
db.insert("a", "1");
std::this_thread::sleep_for(std::chrono::milliseconds(700));
CHECK(db.update("a", "2", true), "update with refresh should succeed");
std::this_thread::sleep_for(std::chrono::milliseconds(600));
CHECK(db.search("a").value() == "2",
"entry alive past original TTL thanks to refresh");
PASS(); PASS();
} }
@@ -344,6 +383,8 @@ int main() {
test_get_keys(); test_get_keys();
test_get_time_touch(); test_get_time_touch();
test_copy_assignment(); test_copy_assignment();
test_copy_constructor();
test_update_refresh_extends_ttl();
test_large_number_of_keys(); test_large_number_of_keys();
test_reinsert_after_remove(); test_reinsert_after_remove();
test_update_without_refresh_keeps_old_ttl(); test_update_without_refresh_keeps_old_ttl();
+6
View File
@@ -197,6 +197,12 @@ def test_expire(c, cnt):
ok, data = c.call("get", key="test:exp") ok, data = c.call("get", key="test:exp")
cnt.check(ok and data == "new", "value after expire", f"got {data}") cnt.check(ok and data == "new", "value after expire", f"got {data}")
ok, data = c.call("expire", key="test:exp:nonexistent", value="x")
cnt.check(ok and data is False, "expire nonexistent key returns false", f"got {data}")
ok, data = c.call("get", key="test:exp:nonexistent")
cnt.check(ok and data is None, "expire nonexistent does not insert", f"got {data}")
def test_missing_params(c, cnt): def test_missing_params(c, cnt):
ok, data = c.call("set", key="only_key") ok, data = c.call("set", key="only_key")