diff --git a/memdb/.gitignore b/memdb/.gitignore index 839a556..1e05e31 100644 --- a/memdb/.gitignore +++ b/memdb/.gitignore @@ -69,4 +69,7 @@ Testing/ .cache/ # Databases -*.db* \ No newline at end of file +*.db* +# unit test binary +test_libmemdb +.memdb.pid diff --git a/memdb/Makefile b/memdb/Makefile index 7d483a5..be0fd5e 100644 --- a/memdb/Makefile +++ b/memdb/Makefile @@ -8,6 +8,9 @@ BUILD = build TARGET = $(BUILD)/memdb SRC = src/memdb.cpp +HOST ?= 127.0.0.1 +PORT ?= 8011 + all: $(TARGET) $(BUILD): @@ -37,13 +40,12 @@ test-libmemdb: $(TEST_LIBMEMDB) test-memdb: $(TARGET) @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 - -python3 test_memdb.py $(HOST) $(PORT); EXIT=$$? - @kill %1 2>/dev/null; wait %1 2>/dev/null - @exit $$EXIT + @python3 test_memdb.py $(HOST) $(PORT); EXIT=$$?; \ + kill $$(cat .memdb.pid) 2>/dev/null; rm -f .memdb.pid; exit $$EXIT clean: - rm -f $(TARGET) $(TEST_LIBMEMDB) + rm -f $(TARGET) $(TEST_LIBMEMDB) .memdb.pid .PHONY: all clean test test-libmemdb test-memdb \ No newline at end of file diff --git a/memdb/README.md b/memdb/README.md index f9b0032..5be9f47 100644 --- a/memdb/README.md +++ b/memdb/README.md @@ -21,7 +21,7 @@ Memdb (libmemdb 模板:LRU 链表 + 哈希索 | `update` | `key`, `value`, `refresh_ttl?` | `true/false` | 更新值,可选刷新 TTL | | `exists` | `key` | `true/false` | 键是否存在 | | `size` | — | `int` | 当前条目数 | -| `expire` | `key`, `value` | `true/false` | 更新值并重置 TTL(del + set) | +| `expire` | `key`, `value` | `true/false` | 更新已有键的值并重置 TTL(键不存在返回 false) | | `show` | — | `[{key,value},...]` | 导出所有条目 | ## 配置 @@ -34,6 +34,7 @@ Memdb (libmemdb 模板:LRU 链表 + 哈希索 | `listen_port` | `8011` | 监听端口 | | `workers` | `4` | 线程池大小 | | `max_time` | `600` | 默认 TTL(秒),10 分钟 | +| `backlog` | `128` | listen() backlog(libmsg 默认 10,短连接突刺易丢连接) | ## 构建与运行 diff --git a/memdb/src/memdb.cpp b/memdb/src/memdb.cpp index 3641cc9..31467f2 100644 --- a/memdb/src/memdb.cpp +++ b/memdb/src/memdb.cpp @@ -20,6 +20,7 @@ struct ServerConfig { int listen_port = 8011; int workers = 4; int max_time = 600; // 默认 TTL 10 分钟 + int backlog = 128; // 短连接并发突刺时防丢连接(libmsg 默认仅 10) }; static std::atomic g_running{true}; @@ -35,7 +36,7 @@ private: pmc::net::RpcServer __rpc; 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); if (it == req.end()) { throw std::runtime_error("missing required field: " + field); @@ -43,7 +44,8 @@ private: if (!it->value().is_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: @@ -51,7 +53,7 @@ public: : __cfg(cfg) , __db(cfg.max_time) , __rpc(cfg.listen_host, cfg.listen_port, - {.workers = cfg.workers}) + {.workers = cfg.workers, .listen_backlog = cfg.backlog}) { _register_routes(); } @@ -130,13 +132,12 @@ private: return result; }); - // ---- expire: 更新值并重置 TTL(等价于 del + set)---- + // ---- expire: 更新值并重置 TTL(仅已有键;等价于 update + refresh_ttl)---- __rpc.add_route("expire", [this](const json::object& req) -> json::value { const auto& key = _require_string(req, "key"); const auto& val = _require_string(req, "value"); std::lock_guard lock(__mtx); - __db.remove(key); - return __db.insert(key, val); + return __db.update(key, val, true); }); } }; @@ -156,7 +157,8 @@ int main(int argc, char** argv) { ("host", po::value()->default_value("127.0.0.1"), "listen host") ("port", po::value()->default_value(8011), "listen port") ("workers", po::value()->default_value(4), "worker threads") - ("max-time", po::value()->default_value(600), "default TTL (seconds)"); + ("max-time", po::value()->default_value(600), "default TTL (seconds)") + ("backlog", po::value()->default_value(128), "listen() backlog"); po::variables_map 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(); cfg.workers = vm["workers"].as(); cfg.max_time = vm["max-time"].as(); + cfg.backlog = vm["backlog"].as(); auto instance = vm["instance"].as(); auto log_level = vm["log-level"].as(); diff --git a/memdb/test_libmemdb.cpp b/memdb/test_libmemdb.cpp index dec6270..6eabcf6 100644 --- a/memdb/test_libmemdb.cpp +++ b/memdb/test_libmemdb.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -266,19 +267,57 @@ static void test_get_time_touch() { PASS(); } -// ── copy assignment ───────────────────────────────────────────────────── +// ── copy semantics ────────────────────────────────────────────────────── static void test_copy_assignment() { TEST("copy assignment"); - Memdb db1; - db1.insert("a", "1"); Memdb 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"); + { + Memdb db1; + db1.insert("a", "1"); + 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"); + } + // 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> cpy; + { + Memdb 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 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(); } @@ -344,6 +383,8 @@ int main() { test_get_keys(); test_get_time_touch(); test_copy_assignment(); + test_copy_constructor(); + test_update_refresh_extends_ttl(); test_large_number_of_keys(); test_reinsert_after_remove(); test_update_without_refresh_keeps_old_ttl(); diff --git a/memdb/test_memdb.py b/memdb/test_memdb.py index 1533b0e..359791c 100644 --- a/memdb/test_memdb.py +++ b/memdb/test_memdb.py @@ -197,6 +197,12 @@ def test_expire(c, cnt): ok, data = c.call("get", key="test:exp") 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): ok, data = c.call("set", key="only_key")