From 3a8c4c0e7746a4499a83285de7645388213b64f4 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 14:11:06 +0200 Subject: [PATCH] feat: extend server --- src/server.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/server.zig b/src/server.zig index cffd170..27cd3c4 100644 --- a/src/server.zig +++ b/src/server.zig @@ -5964,3 +5964,162 @@ test "server metrics register into an external Prometheus registry" { try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_ttl_swept_total 0\n") != null); } +test "client.metricsText fetches the server's own counters and db stats over the wire in one payload" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + try client.set("a", "1"); + try client.set("b", "2"); + try client.ping(); + + const text = try client.metricsText(allocator); + defer allocator.free(text); + + // Server's own request counter: 2 sets + 1 ping + this very metrics + // request itself, all counted in `requestsTotal` before dispatch even + // looks at the opcode. + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_server_requests_total 4\n") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_server_rate_limited_total 0\n") != null); + // Db stats rendered by the same `formatDbStats` `stats --prom` uses + // locally, appended after the server's own counters in one response. + try std.testing.expect(std.mem.indexOf(u8, text, "cobalt_memtable_entries 2\n") != null); +} + +test "server gates the metrics opcode behind readonly role like every other read op" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const store = authmod.Store.init(&db); + try store.createUser(allocator, "reader", "pw", .readonly, 16); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var unauthed = try client_mod.Client.connect(allocator, server.boundAddress()); + defer unauthed.close(); + try std.testing.expectError(error.ServerUnauthorized, unauthed.metricsText(allocator)); + + var reader = try client_mod.Client.connect(allocator, server.boundAddress()); + defer reader.close(); + try reader.authenticate("reader", "pw"); + const text = try reader.metricsText(allocator); + allocator.free(text); +} + +test "enableSessionSweep periodically drops expired sessions nobody ever presents again" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + const store = authmod.Store.init(&db); + try store.createUser(allocator, "alice", "hunter2", .readwrite, 16); + + var sessions = sessionmod.SessionStore.init(allocator); + defer sessions.deinit(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + // A short-lived session that nobody will ever present again: without + // the sweeper, this sits in `sessions` forever since `validate()` only + // cleans up a token someone actually tries to use. + server.enableSessions(&sessions, 100 * std.time.ns_per_ms); + server.enableSessionSweep(50 * std.time.ns_per_ms); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + try client.authenticate("alice", "hunter2"); + client.close(); + + try std.testing.expectEqual(@as(usize, 1), sessions.count()); + + // Long enough for the session to expire (100ms) and for at least one + // sweep tick (every 50ms) to have run past that point. + std.time.sleep(400 * std.time.ns_per_ms); + + try std.testing.expectEqual(@as(usize, 0), sessions.count()); + try std.testing.expect(server.sessionsSweptTotal.get() >= 1); +} + +test "a server with sessions enabled but no sweep interval never sweeps" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + const store = authmod.Store.init(&db); + try store.createUser(allocator, "alice", "hunter2", .readwrite, 16); + + var sessions = sessionmod.SessionStore.init(allocator); + defer sessions.deinit(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + server.enableSessions(&sessions, 50 * std.time.ns_per_ms); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + try client.authenticate("alice", "hunter2"); + client.close(); + + std.time.sleep(200 * std.time.ns_per_ms); + + // The session expired a while ago, but with no sweeper running it is + // still sitting in the store's map until something calls `validate` on + // it - `count()` still sees it, unlike the sweeper test above. + try std.testing.expectEqual(@as(usize, 1), sessions.count()); + try std.testing.expectEqual(@as(u64, 0), server.sessionsSweptTotal.get()); +} + +test "ttl_op over the wire: expire, query, persist, and sweep round trip against a real server" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); +