feat: grow server

This commit is contained in:
dualfroz committed 2026-09-12 15:25:08 +02:00
1 parent 7d6fddf3ef
commit 8f9376fed0
1 file changed
+160
+160
View File
@@ -6440,3 +6440,163 @@ test "enableCollTtlSweep periodically reaps due collections nobody ever touches
var client = try client_mod.Client.connect(allocator, server.boundAddress());
defer client.close();
_ = try collmod.ZSet.add(&db, allocator, "board", "alice", 10);
try client.collTtlExpire(.zset, "board", 0);
// Long enough for the collection to be due and for at least one sweep
// tick (every 50ms) to run past that point - nobody ever reads "board"
// again, so without the background sweeper it would sit there forever.
std.time.sleep(300 * std.time.ns_per_ms);
try std.testing.expect(!try collmod.collectionExists(&db, allocator, .zset, "board"));
try std.testing.expect(server.collTtlSweptTotal.get() >= 1);
}
test "a server with no coll ttl sweep interval never runs the coll ttl sweeper" {
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 collmod.Set.add(&db, allocator, "tags", "zig");
try client.collTtlExpire(.set, "tags", 0);
std.time.sleep(100 * std.time.ns_per_ms);
// Nothing swept it in the background, but the collection IS logically
// due - it is still physically present until something sweeps it.
try std.testing.expectEqual(@as(u64, 0), server.collTtlSweptTotal.get());
try std.testing.expect(try collmod.collectionExists(&db, allocator, .set, "tags"));
}
test "a server with the flat ttl sweeper and the coll ttl sweeper enabled at once runs both independently" {
// Every prior session's notes on both sweepers flagged the same gap:
// `enableTtlSweep` and `enableCollTtlSweep` start two structurally
// independent background threads on the same `Server` (separate
// fields, separate `std.Thread.spawn` calls, separate join calls in
// `stop()`), but nothing had ever exercised them running at the same
// time. This proves three things at once: a flat key due for
// expiry and a whole collection due for expiry both get reaped by
// their OWN sweeper without either one starving or double-counting
// the other's target, and `stop()` cleanly joins both sweeper threads
// (the exact bug class caught and fixed in session #28, where a
// missing join on one sweeper thread left it alive past `db.close()`
// and segfaulted the next test).
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);
server.enableTtlSweep(50 * std.time.ns_per_ms);
server.enableCollTtlSweep(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());
defer client.close();
try client.set("flatkey", "1");
try client.ttlExpire("flatkey", 0);
_ = try collmod.Hash.set(&db, allocator, "session:1", "user", "ada");
try client.collTtlExpire(.hash, "session:1", 0);
// Poll rather than sleep a single fixed window: two background
// sweeper threads plus the accept/connection threads contending for
// `self.mutex` on a loaded host can occasionally push either sweeper's
// tick past a fixed 300ms, which is not a correctness bug in the
// sweeper - it is exactly the kind of host-load flakiness this
// project's own frostbyte sessions document repeatedly. Waiting up to
// 3s in 50ms increments keeps the assertion meaningful (still fails
// for real if a sweeper never runs) without being a coin flip on a
// busy machine.
var waited: u64 = 0;
while (waited < 3000 and (server.ttlSweptTotal.get() == 0 or server.collTtlSweptTotal.get() == 0)) {
std.time.sleep(50 * std.time.ns_per_ms);
waited += 50;
}
try std.testing.expectEqual(@as(?[]u8, null), try client.get(allocator, "flatkey"));
try std.testing.expect(!try collmod.collectionExists(&db, allocator, .hash, "session:1"));
try std.testing.expect(server.ttlSweptTotal.get() >= 1);
try std.testing.expect(server.collTtlSweptTotal.get() >= 1);
// Neither sweeper's counter absorbed the other's reap: exactly one
// flat key was due and exactly one collection was due, so each
// counter reads exactly 1, not a shared/blended total.
try std.testing.expectEqual(@as(u64, 1), server.ttlSweptTotal.get());
try std.testing.expectEqual(@as(u64, 1), server.collTtlSweptTotal.get());
}
test "coll_op over the wire exercises all five collection kinds 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();
const client_mod = @import("client.zig");
var client = try client_mod.Client.connect(allocator, server.boundAddress());
defer client.close();
// Counter: get on a name nobody ever touched defaults to 0, set
// pins an exact value, incrby accumulates - the same three-step
// shape `collections.test.counter incrBy accumulates` already
// covers locally, now round-tripped over the wire.
{
var got = try client.collOp(allocator, .counter_get, "visits", &.{});
defer got.deinit(allocator);
try std.testing.expectEqual(@as(i64, 0), got.int);
var setReply = try client.collOp(allocator, .counter_set, "visits", &[_][]const u8{"10"});
defer setReply.deinit(allocator);
try std.testing.expect(setReply == .ok);
var incrReply = try client.collOp(allocator, .counter_incrby, "visits", &[_][]const u8{"5"});
defer incrReply.deinit(allocator);
try std.testing.expectEqual(@as(i64, 15), incrReply.int);
}
// Hash: set reports created vs updated as 1/0, get returns the
// value or nil, del removes a field, len/getall reflect what remains.
{
var created = try client.collOp(allocator, .hash_set, "profile", &[_][]const u8{ "name", "ada" });
defer created.deinit(allocator);
try std.testing.expectEqual(@as(i64, 1), created.int);
var updated = try client.collOp(allocator, .hash_set, "profile", &[_][]const u8{ "name", "grace" });
defer updated.deinit(allocator);
try std.testing.expectEqual(@as(i64, 0), updated.int);
_ = try client.collOp(allocator, .hash_set, "profile", &[_][]const u8{ "age", "36" });
var got = try client.collOp(allocator, .hash_get, "profile", &[_][]const u8{"name"});
defer got.deinit(allocator);
try std.testing.expectEqualStrings("grace", got.bytes);