feat: add WriteCtx to server
This commit is contained in:
1 parent
638c66a0ab
commit
c6e742fc4f
1 file changed
+155
+155
@@ -6911,3 +6911,158 @@ test "coll_op set_inter/union/diff lazily expire every extra set name, not just
|
|||||||
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.Set.add(&db, allocator, "a", "x");
|
||||||
|
_ = try collmod.Set.add(&db, allocator, "b", "x");
|
||||||
|
try client.collTtlExpire(.set, "b", 0);
|
||||||
|
std.time.sleep(5 * std.time.ns_per_ms);
|
||||||
|
|
||||||
|
// "b" is due but is only referenced through `args`, never `name` - if
|
||||||
|
// the handler only checked `name` (like it does for every single-name
|
||||||
|
// action), "b" would still look alive here and `intersect` would see
|
||||||
|
// its stale "x" member. It must come back empty instead.
|
||||||
|
var inter = try client.collOp(allocator, .set_inter, "a", &[_][]const u8{"b"});
|
||||||
|
defer inter.deinit(allocator);
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), inter.stringList.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "coll_op gates a read action as readonly and a write action as readwrite" {
|
||||||
|
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();
|
||||||
|
_ = try collmod.Hash.set(&db, allocator, "k", "f", "v");
|
||||||
|
|
||||||
|
const store = authmod.Store.init(&db);
|
||||||
|
try store.createUser(allocator, "reader", "pw", .readonly, 16);
|
||||||
|
try store.createUser(allocator, "writer", "pw", .readwrite, 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 writer = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer writer.close();
|
||||||
|
try writer.authenticate("writer", "pw");
|
||||||
|
|
||||||
|
var reader = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer reader.close();
|
||||||
|
try reader.authenticate("reader", "pw");
|
||||||
|
|
||||||
|
// hash_get is read-only: a readonly connection can run it.
|
||||||
|
var got = try reader.collOp(allocator, .hash_get, "k", &[_][]const u8{"f"});
|
||||||
|
defer got.deinit(allocator);
|
||||||
|
try std.testing.expectEqualStrings("v", got.bytes);
|
||||||
|
|
||||||
|
// hash_set mutates: forbidden for a readonly connection.
|
||||||
|
try std.testing.expectError(client_mod.ClientError.ServerForbidden, reader.collOp(allocator, .hash_set, "k", &[_][]const u8{ "f", "v2" }));
|
||||||
|
|
||||||
|
// The readwrite connection can do it. "f" already exists, so hash_set
|
||||||
|
// reports it as updated (0), not created (1) - same int-flag shape as
|
||||||
|
// set_add/set_rem/hash_del elsewhere in this opcode, not `.ok`.
|
||||||
|
var setReply = try writer.collOp(allocator, .hash_set, "k", &[_][]const u8{ "f", "v2" });
|
||||||
|
defer setReply.deinit(allocator);
|
||||||
|
try std.testing.expectEqual(@as(i64, 0), setReply.int);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server rejects a malformed coll_op frame with bad_request without crashing" {
|
||||||
|
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 stream = try std.net.tcpConnectToAddress(server.boundAddress());
|
||||||
|
defer stream.close();
|
||||||
|
|
||||||
|
// A single action byte is not enough to decode the length-prefixed
|
||||||
|
// name that must follow it.
|
||||||
|
try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.coll_op), "\x00");
|
||||||
|
var frame = try protocol.readFrame(allocator, stream.reader());
|
||||||
|
defer frame.deinit();
|
||||||
|
try std.testing.expectEqual(@intFromEnum(protocol.Status.bad_request), frame.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server answers coll_op with bad_request when an action gets too few arguments" {
|
||||||
|
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_set needs a value argument - a well-formed frame that omits
|
||||||
|
// it should come back as a clean bad_request, not an internal error or
|
||||||
|
// a crash from indexing into an empty args slice.
|
||||||
|
try std.testing.expectError(client_mod.ClientError.ServerBadRequest, client.collOp(allocator, .counter_set, "n", &.{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same ksztalt of proof as the two `coll_op` concurrency tests above
|
||||||
|
// (many real OS threads, each its own TCP connection, hammering shared
|
||||||
|
// server state) but for `ttl_op` instead - the next direction flagged at
|
||||||
|
// the end of session solo #32 (cobalt-postep.md). `ttl_op` has no shared
|
||||||
|
// mutable VALUE the way a counter or set does (each writer's key is its
|
||||||
|
// own, disjoint from every other writer's), so the race worth proving
|
||||||
|
// here is different: writer connections racing `set`+`expire` against
|
||||||
|
// sweeper connections racing `sweep`, all against the SAME `Db.sstables`/
|
||||||
|
// memtable underneath. `ttl.sweepExpired` reads its own prefix scan and
|
||||||
|
// deletes what it found within a single `handleTtlOp` call, which the
|
||||||
|
// `self.mutex` in `Server.dispatch` executes atomically end-to-end, so a
|
||||||
|
// key can never be swept twice and no swept count can double-count - the
|
||||||
|
// grand total across every sweep response plus one final mop-up sweep
|
||||||
|
// must land on exactly the number of keys written, with zero lost and
|
||||||
|
// zero duplicated, no matter how the OS interleaves the connections.
|
||||||
|
test "ttl_op survives many real concurrent connections racing writes against 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 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");
|
||||||
|
const writerCount = 6;
|
||||||
|
const perWriter = 40;
|
||||||
|
const sweeperCount = 3;
|
||||||
|
const sweepsPerSweeper = 200;
|
||||||
|
|
||||||
|
const WriteCtx = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
addr: std.net.Address,
|
||||||
|
thread: usize,
|
||||||
|
ok: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
Reference in new issue
Block a user