feat: add ReaderCtx and readerWorker to server
This commit is contained in:
1 parent
7bd98b0580
commit
8ebd5378b2
1 file changed
+157
+157
@@ -2645,3 +2645,160 @@ test "server rejects a malformed table_update frame (bad set fields) with bad_re
|
||||
try std.testing.expectEqual(@intFromEnum(protocol.Status.ok), pingFrame.tag);
|
||||
}
|
||||
|
||||
// `table_update` shares the same underlying-sstable-reshaping hazard as
|
||||
// `table_select`/`table_join`/`table_aggregate` above, but with a sharper
|
||||
// edge: `updateWhere` both READS (via `selectAllMatches`) and WRITES (via
|
||||
// `put`) in the same request, while those three only read. `self.mutex` in
|
||||
// `Server.dispatch` still serializes every request end-to-end including
|
||||
// `flush`/`compact`, so no single request can observe a torn sstable set -
|
||||
// what this test actually proves is that real structural reshaping,
|
||||
// interleaved via real OS threads through the real TCP/dispatch path, never
|
||||
// desyncs `updateWhere`'s read-then-write from the index it must keep in
|
||||
// sync, and that a single updater thread's own sequence of writes lands
|
||||
// exactly where expected once every other thread has stopped.
|
||||
//
|
||||
// Thirty rows split evenly three ways by `@mod(pk, 3)` (same helper as the
|
||||
// `table_aggregate` concurrency test above) so the "city" index always
|
||||
// holds exactly 10 rows per bucket - that count must never move, because
|
||||
// this test's single updater thread only ever touches the "epoch" field,
|
||||
// never "city" itself, so index population is an invariant a reader can
|
||||
// check at every single snapshot, not just at the end.
|
||||
test "table_update survives concurrent flush and compact reshaping the underlying sstables" {
|
||||
const allocator = std.testing.allocator;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
const path = try tmp.dir.realpathAlloc(allocator, ".");
|
||||
defer allocator.free(path);
|
||||
|
||||
// Small target size so a handful of `flush`/`compact` calls do real
|
||||
// structural work instead of needing megabytes of data first - same
|
||||
// trick as the `table_select`/`table_join`/`table_aggregate`
|
||||
// concurrency tests above.
|
||||
var db = try Db.open(allocator, path, .{ .sstableTargetBytes = 512 });
|
||||
defer db.close();
|
||||
|
||||
const rowCount = 30;
|
||||
{
|
||||
var accounts = try querymod.Table.open(&db, allocator, "accounts");
|
||||
defer accounts.deinit();
|
||||
try accounts.createIndex("city");
|
||||
var pk: i64 = 1;
|
||||
while (pk <= rowCount) : (pk += 1) {
|
||||
const city: []const u8 = switch (@mod(pk, 3)) {
|
||||
0 => "warsaw",
|
||||
1 => "krakow",
|
||||
else => "gdansk",
|
||||
};
|
||||
try accounts.put(.{ .int = pk }, &.{
|
||||
.{ .name = "city", .value = .{ .text = city } },
|
||||
.{ .name = "epoch", .value = .{ .int = 0 } },
|
||||
});
|
||||
}
|
||||
try db.flush();
|
||||
}
|
||||
|
||||
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 readerThreads = 4;
|
||||
const readerIters = 20;
|
||||
const reshapeThreads = 2;
|
||||
const reshapeIters = 15;
|
||||
const updaterIters = 20;
|
||||
|
||||
const ReaderCtx = struct {
|
||||
allocator: Allocator,
|
||||
addr: std.net.Address,
|
||||
ok: bool = false,
|
||||
};
|
||||
const readerWorker = struct {
|
||||
fn run(ctx: *ReaderCtx) void {
|
||||
var client = client_mod.Client.connect(ctx.allocator, ctx.addr) catch return;
|
||||
defer client.close();
|
||||
var i: usize = 0;
|
||||
while (i < readerIters) : (i += 1) {
|
||||
inline for (.{ "warsaw", "krakow", "gdansk" }) |city| {
|
||||
const buf = std.fmt.allocPrint(ctx.allocator, "city eq s:{s}", .{city}) catch return;
|
||||
defer ctx.allocator.free(buf);
|
||||
const rows = client.tableSelect(ctx.allocator, "accounts", buf, 0, 0, &.{}) catch return;
|
||||
defer protocol.freeRows(ctx.allocator, rows);
|
||||
if (rows.len != 10) return;
|
||||
for (rows) |row| {
|
||||
const got = row.get("city") orelse return;
|
||||
if (!std.mem.eql(u8, got.text, city)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.ok = true;
|
||||
}
|
||||
}.run;
|
||||
|
||||
const ReshapeCtx = struct {
|
||||
allocator: Allocator,
|
||||
addr: std.net.Address,
|
||||
thread: usize,
|
||||
ok: bool = false,
|
||||
};
|
||||
const reshapeWorker = struct {
|
||||
fn run(ctx: *ReshapeCtx) void {
|
||||
var client = client_mod.Client.connect(ctx.allocator, ctx.addr) catch return;
|
||||
defer client.close();
|
||||
var i: usize = 0;
|
||||
while (i < reshapeIters) : (i += 1) {
|
||||
var buf: [32]u8 = undefined;
|
||||
const key = std.fmt.bufPrint(&buf, "update-churn-{d}-{d}", .{ ctx.thread, i }) catch return;
|
||||
client.set(key, "x") catch return;
|
||||
client.flush() catch return;
|
||||
client.compact() catch return;
|
||||
}
|
||||
ctx.ok = true;
|
||||
}
|
||||
}.run;
|
||||
|
||||
const UpdaterCtx = struct {
|
||||
allocator: Allocator,
|
||||
addr: std.net.Address,
|
||||
ok: bool = false,
|
||||
};
|
||||
const updaterWorker = struct {
|
||||
fn run(ctx: *UpdaterCtx) void {
|
||||
var client = client_mod.Client.connect(ctx.allocator, ctx.addr) catch return;
|
||||
defer client.close();
|
||||
var i: i64 = 0;
|
||||
while (i < updaterIters) : (i += 1) {
|
||||
const target: []const u8 = if (@mod(i, 2) == 0) "krakow" else "gdansk";
|
||||
const expr = std.fmt.allocPrint(ctx.allocator, "city eq s:{s}", .{target}) catch return;
|
||||
defer ctx.allocator.free(expr);
|
||||
const sets = [_]rowmod.Field{.{ .name = "epoch", .value = .{ .int = i } }};
|
||||
const updated = client.tableUpdate("accounts", &sets, expr) catch return;
|
||||
if (updated != 10) return;
|
||||
}
|
||||
ctx.ok = true;
|
||||
}
|
||||
}.run;
|
||||
|
||||
var readerContexts: [readerThreads]ReaderCtx = undefined;
|
||||
var readerHandles: [readerThreads]std.Thread = undefined;
|
||||
for (0..readerThreads) |i| {
|
||||
readerContexts[i] = .{ .allocator = allocator, .addr = server.boundAddress() };
|
||||
readerHandles[i] = try std.Thread.spawn(.{}, readerWorker, .{&readerContexts[i]});
|
||||
}
|
||||
var reshapeContexts: [reshapeThreads]ReshapeCtx = undefined;
|
||||
var reshapeHandles: [reshapeThreads]std.Thread = undefined;
|
||||
for (0..reshapeThreads) |i| {
|
||||
reshapeContexts[i] = .{ .allocator = allocator, .addr = server.boundAddress(), .thread = i };
|
||||
reshapeHandles[i] = try std.Thread.spawn(.{}, reshapeWorker, .{&reshapeContexts[i]});
|
||||
}
|
||||
var updaterContext = UpdaterCtx{ .allocator = allocator, .addr = server.boundAddress() };
|
||||
const updaterHandle = try std.Thread.spawn(.{}, updaterWorker, .{&updaterContext});
|
||||
|
||||
for (0..readerThreads) |i| readerHandles[i].join();
|
||||
for (0..reshapeThreads) |i| reshapeHandles[i].join();
|
||||
updaterHandle.join();
|
||||
for (0..readerThreads) |i| try std.testing.expect(readerContexts[i].ok);
|
||||
for (0..reshapeThreads) |i| try std.testing.expect(reshapeContexts[i].ok);
|
||||
try std.testing.expect(updaterContext.ok);
|
||||
|
||||
Reference in new issue
Block a user