feat: add WorkerCtx and worker to server
This commit is contained in:
1 parent
44c5e9b9cb
commit
48ff5cf38e
1 file changed
+159
+159
@@ -2171,3 +2171,162 @@ test "server batch applies puts and deletes atomically" {
|
|||||||
try std.testing.expect(kept == null);
|
try std.testing.expect(kept == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "server rejects a malformed 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();
|
||||||
|
|
||||||
|
try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.set), "not a valid set payload with no length prefix boundary");
|
||||||
|
var frame = try protocol.readFrame(allocator, stream.reader());
|
||||||
|
defer frame.deinit();
|
||||||
|
try std.testing.expectEqual(@intFromEnum(protocol.Status.bad_request), frame.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server serves multiple concurrent clients without corrupting state" {
|
||||||
|
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 WorkerCtx = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
addr: std.net.Address,
|
||||||
|
index: usize,
|
||||||
|
ok: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const worker = struct {
|
||||||
|
fn run(ctx: *WorkerCtx) void {
|
||||||
|
var client = client_mod.Client.connect(ctx.allocator, ctx.addr) catch return;
|
||||||
|
defer client.close();
|
||||||
|
var buf: [32]u8 = undefined;
|
||||||
|
const key = std.fmt.bufPrint(&buf, "k{d}", .{ctx.index}) catch return;
|
||||||
|
client.set(key, "v") catch return;
|
||||||
|
const got = client.get(ctx.allocator, key) catch return;
|
||||||
|
defer if (got) |g| ctx.allocator.free(g);
|
||||||
|
ctx.ok = got != null and std.mem.eql(u8, got.?, "v");
|
||||||
|
}
|
||||||
|
}.run;
|
||||||
|
|
||||||
|
var contexts: [8]WorkerCtx = undefined;
|
||||||
|
var threads: [8]std.Thread = undefined;
|
||||||
|
for (0..8) |i| {
|
||||||
|
contexts[i] = .{ .allocator = allocator, .addr = server.boundAddress(), .index = i };
|
||||||
|
threads[i] = try std.Thread.spawn(.{}, worker, .{&contexts[i]});
|
||||||
|
}
|
||||||
|
for (0..8) |i| threads[i].join();
|
||||||
|
for (0..8) |i| try std.testing.expect(contexts[i].ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server enforces a token-bucket rate limit and rejects the overflow" {
|
||||||
|
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);
|
||||||
|
// Slow refill (1 token/sec): the whole burst below completes in well
|
||||||
|
// under a second of wall-clock time, so refill contributes nowhere
|
||||||
|
// near a full token and the outcome is deterministic.
|
||||||
|
server.enableRateLimit(3, 1);
|
||||||
|
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();
|
||||||
|
|
||||||
|
var allowed: usize = 0;
|
||||||
|
var rejected: usize = 0;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < 6) : (i += 1) {
|
||||||
|
client.ping() catch |err| {
|
||||||
|
try std.testing.expectEqual(client_mod.ClientError.ServerRateLimited, err);
|
||||||
|
rejected += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
allowed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 3), allowed);
|
||||||
|
try std.testing.expectEqual(@as(usize, 3), rejected);
|
||||||
|
try std.testing.expectEqual(@as(u64, 6), server.requestsTotal.get());
|
||||||
|
try std.testing.expectEqual(@as(u64, 3), server.rateLimitedTotal.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server without an enabled rate limit never rejects" {
|
||||||
|
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();
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < 20) : (i += 1) try client.ping();
|
||||||
|
try std.testing.expectEqual(@as(u64, 20), server.requestsTotal.get());
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), server.rateLimitedTotal.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "per-connection rate limit gives each connection its own budget" {
|
||||||
|
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.enablePerConnectionRateLimit(2, 1);
|
||||||
|
try server.start();
|
||||||
|
defer server.stop();
|
||||||
|
|
||||||
|
const client_mod = @import("client.zig");
|
||||||
|
|
||||||
|
var clientA = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer clientA.close();
|
||||||
|
var clientB = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer clientB.close();
|
||||||
|
|
||||||
Reference in new issue
Block a user