feat: expand server
This commit is contained in:
1 parent
3a8c4c0e77
commit
c741ac045f
1 file changed
+159
+159
@@ -6123,3 +6123,162 @@ test "ttl_op over the wire: expire, query, persist, and sweep round trip against
|
||||
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();
|
||||
|
||||
// A key with no ttl at all reads back .noExpiry, not .noKey.
|
||||
try client.set("plain", "v");
|
||||
try std.testing.expectEqual(ttlmod.TtlResult.noExpiry, try client.ttlQuery("plain"));
|
||||
|
||||
// expire on a key that does not exist is a real, distinct failure.
|
||||
try std.testing.expectError(client_mod.ClientError.ServerKeyNotFound, client.ttlExpire("ghost", 60));
|
||||
|
||||
// expire on an existing key sets a marker the query action can see.
|
||||
// Real wall-clock time (both `expire` and `query` stamp their own
|
||||
// `std.time.nanoTimestamp()` server-side) can shave a second off by
|
||||
// the time the second round trip lands, so accept 59 or 60 rather
|
||||
// than pinning an exact value.
|
||||
try client.ttlExpire("plain", 60);
|
||||
const withTtl = try client.ttlQuery("plain");
|
||||
try std.testing.expect(withTtl.seconds == 59 or withTtl.seconds == 60);
|
||||
|
||||
// persist removes the marker again, back to .noExpiry.
|
||||
try client.ttlPersist("plain");
|
||||
try std.testing.expectEqual(ttlmod.TtlResult.noExpiry, try client.ttlQuery("plain"));
|
||||
|
||||
// sweep over a key that is already due for expiry reaps exactly it,
|
||||
// and a real subsequent get proves the delete was real, not marker-only.
|
||||
try client.set("doomed", "x");
|
||||
try client.ttlExpire("doomed", 0);
|
||||
std.time.sleep(5 * std.time.ns_per_ms);
|
||||
const reaped = try client.ttlSweep();
|
||||
try std.testing.expectEqual(@as(u64, 1), reaped);
|
||||
try std.testing.expectEqual(@as(?[]u8, null), try client.get(allocator, "doomed"));
|
||||
|
||||
// querying a key that was never there at all (not merely expired) is
|
||||
// .noKey, distinct from .noExpiry.
|
||||
try std.testing.expectEqual(ttlmod.TtlResult.noKey, try client.ttlQuery("never-existed"));
|
||||
}
|
||||
|
||||
test "ttl_op gates query as readonly and expire 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();
|
||||
|
||||
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");
|
||||
try writer.set("k", "v");
|
||||
|
||||
var reader = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||
defer reader.close();
|
||||
try reader.authenticate("reader", "pw");
|
||||
|
||||
// query is readonly: a readonly connection can run it.
|
||||
try std.testing.expectEqual(ttlmod.TtlResult.noExpiry, try reader.ttlQuery("k"));
|
||||
|
||||
// expire/persist/sweep all mutate: forbidden for a readonly connection.
|
||||
try std.testing.expectError(client_mod.ClientError.ServerForbidden, reader.ttlExpire("k", 30));
|
||||
try std.testing.expectError(client_mod.ClientError.ServerForbidden, reader.ttlPersist("k"));
|
||||
try std.testing.expectError(client_mod.ClientError.ServerForbidden, reader.ttlSweep());
|
||||
|
||||
// The readwrite connection can do all four.
|
||||
try writer.ttlExpire("k", 30);
|
||||
try writer.ttlPersist("k");
|
||||
_ = try writer.ttlSweep();
|
||||
}
|
||||
|
||||
test "server rejects a malformed ttl_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();
|
||||
|
||||
try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.ttl_op), "\x09");
|
||||
var frame = try protocol.readFrame(allocator, stream.reader());
|
||||
defer frame.deinit();
|
||||
try std.testing.expectEqual(@intFromEnum(protocol.Status.bad_request), frame.tag);
|
||||
}
|
||||
|
||||
test "enableTtlSweep periodically reaps due keys nobody ever reads 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 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);
|
||||
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.ttlExpire("a", 0);
|
||||
|
||||
// Long enough for the key to be due and for at least one sweep tick
|
||||
// (every 50ms) to run past that point - nobody ever reads "a" again,
|
||||
// so without the background sweeper it would sit there forever (only
|
||||
// `ttl.getLive`'s lazy path or an explicit sweep would ever catch it).
|
||||
std.time.sleep(300 * std.time.ns_per_ms);
|
||||
|
||||
try std.testing.expectEqual(@as(?[]u8, null), try client.get(allocator, "a"));
|
||||
try std.testing.expect(server.ttlSweptTotal.get() >= 1);
|
||||
}
|
||||
|
||||
test "a server with no ttl sweep interval never runs the 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();
|
||||
|
||||
Reference in new issue
Block a user