feat: grow server
This commit is contained in:
1 parent
7d6fddf3ef
commit
8f9376fed0
1 file changed
+160
+160
@@ -6440,3 +6440,163 @@ test "enableCollTtlSweep periodically reaps due collections nobody ever touches
|
|||||||
var client = try client_mod.Client.connect(allocator, server.boundAddress());
|
var client = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
defer client.close();
|
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);
|
||||||
|
|
||||||
Reference in new issue
Block a user