feat: add readerWorker and run to server
This commit is contained in:
1 parent
eee83a0ec7
commit
1f8f067bfc
1 file changed
+160
+160
@@ -5169,3 +5169,163 @@ test "table_select survives concurrent flush and compact reshaping the underlyin
|
|||||||
.{ .field = "age", .descending = true },
|
.{ .field = "age", .descending = true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const warsawRows = client.tableSelect(ctx.allocator, "people", "city eq s:warsaw", 0, 0, &.{}) catch return;
|
||||||
|
defer protocol.freeRows(ctx.allocator, warsawRows);
|
||||||
|
if (warsawRows.len != rowCount / 3) return;
|
||||||
|
|
||||||
|
const allRows = client.tableSelect(ctx.allocator, "people", "", 0, 0, &.{}) catch return;
|
||||||
|
defer protocol.freeRows(ctx.allocator, allRows);
|
||||||
|
if (allRows.len != rowCount) return;
|
||||||
|
for (allRows) |row| {
|
||||||
|
const pk = row.get(querymod.pkField) orelse return;
|
||||||
|
const age = row.get("age") orelse return;
|
||||||
|
if (age.int != pk.int * 2) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordered = client.tableSelect(ctx.allocator, "people", "", 0, 0, &cityOrderBy) catch return;
|
||||||
|
defer protocol.freeRows(ctx.allocator, ordered);
|
||||||
|
if (ordered.len != rowCount) return;
|
||||||
|
var prevCity: ?[]const u8 = null;
|
||||||
|
var prevAge: i64 = std.math.maxInt(i64);
|
||||||
|
for (ordered) |row| {
|
||||||
|
const city = (row.get("city") orelse return).text;
|
||||||
|
const age = (row.get("age") orelse return).int;
|
||||||
|
if (prevCity) |pc| {
|
||||||
|
const cmp = std.mem.order(u8, pc, city);
|
||||||
|
if (cmp == .gt) return;
|
||||||
|
if (cmp == .eq and age > prevAge) return;
|
||||||
|
if (cmp != .eq) prevAge = std.math.maxInt(i64);
|
||||||
|
}
|
||||||
|
prevCity = city;
|
||||||
|
prevAge = age;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.ok = true;
|
||||||
|
}
|
||||||
|
}.run;
|
||||||
|
|
||||||
|
const MutatorCtx = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
addr: std.net.Address,
|
||||||
|
thread: usize,
|
||||||
|
ok: bool = false,
|
||||||
|
};
|
||||||
|
const mutatorWorker = struct {
|
||||||
|
fn run(ctx: *MutatorCtx) void {
|
||||||
|
var client = client_mod.Client.connect(ctx.allocator, ctx.addr) catch return;
|
||||||
|
defer client.close();
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < mutatorIters) : (i += 1) {
|
||||||
|
var buf: [32]u8 = undefined;
|
||||||
|
const key = std.fmt.bufPrint(&buf, "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;
|
||||||
|
|
||||||
|
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 mutatorContexts: [mutatorThreads]MutatorCtx = undefined;
|
||||||
|
var mutatorHandles: [mutatorThreads]std.Thread = undefined;
|
||||||
|
for (0..mutatorThreads) |i| {
|
||||||
|
mutatorContexts[i] = .{ .allocator = allocator, .addr = server.boundAddress(), .thread = i };
|
||||||
|
mutatorHandles[i] = try std.Thread.spawn(.{}, mutatorWorker, .{&mutatorContexts[i]});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0..readerThreads) |i| readerHandles[i].join();
|
||||||
|
for (0..mutatorThreads) |i| mutatorHandles[i].join();
|
||||||
|
for (0..readerThreads) |i| try std.testing.expect(readerContexts[i].ok);
|
||||||
|
for (0..mutatorThreads) |i| try std.testing.expect(mutatorContexts[i].ok);
|
||||||
|
|
||||||
|
// Final sanity check on a fresh connection: after all the concurrent
|
||||||
|
// churn, the table data itself must be byte-for-byte what was written
|
||||||
|
// before the server ever started - `flush`/`compact` change physical
|
||||||
|
// layout, never live values.
|
||||||
|
var client = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer client.close();
|
||||||
|
const finalAll = try client.tableSelect(allocator, "people", "", 0, 0, &.{});
|
||||||
|
defer protocol.freeRows(allocator, finalAll);
|
||||||
|
try std.testing.expectEqual(@as(usize, rowCount), finalAll.len);
|
||||||
|
const finalWarsaw = try client.tableSelect(allocator, "people", "city eq s:warsaw", 0, 0, &.{});
|
||||||
|
defer protocol.freeRows(allocator, finalWarsaw);
|
||||||
|
try std.testing.expectEqual(@as(usize, rowCount / 3), finalWarsaw.len);
|
||||||
|
|
||||||
|
// Precise check of the multi-column order-by: city groups sorted
|
||||||
|
// alphabetically (gdansk < krakow < warsaw), each group internally by
|
||||||
|
// age (== pk*2) descending. pk%3 maps 2->gdansk, 1->krakow, 0->warsaw,
|
||||||
|
// so each group's highest pk (== highest age) leads that group.
|
||||||
|
const finalOrdered = try client.tableSelect(allocator, "people", "", 0, 0, &cityOrderBy);
|
||||||
|
defer protocol.freeRows(allocator, finalOrdered);
|
||||||
|
try std.testing.expectEqual(@as(usize, rowCount), finalOrdered.len);
|
||||||
|
try std.testing.expectEqualStrings("gdansk", finalOrdered[0].get("city").?.text);
|
||||||
|
try std.testing.expectEqual(@as(i64, 29), finalOrdered[0].get(querymod.pkField).?.int);
|
||||||
|
try std.testing.expectEqualStrings("gdansk", finalOrdered[9].get("city").?.text);
|
||||||
|
try std.testing.expectEqual(@as(i64, 2), finalOrdered[9].get(querymod.pkField).?.int);
|
||||||
|
try std.testing.expectEqualStrings("krakow", finalOrdered[10].get("city").?.text);
|
||||||
|
try std.testing.expectEqual(@as(i64, 28), finalOrdered[10].get(querymod.pkField).?.int);
|
||||||
|
try std.testing.expectEqualStrings("krakow", finalOrdered[19].get("city").?.text);
|
||||||
|
try std.testing.expectEqual(@as(i64, 1), finalOrdered[19].get(querymod.pkField).?.int);
|
||||||
|
try std.testing.expectEqualStrings("warsaw", finalOrdered[20].get("city").?.text);
|
||||||
|
try std.testing.expectEqual(@as(i64, 30), finalOrdered[20].get(querymod.pkField).?.int);
|
||||||
|
try std.testing.expectEqualStrings("warsaw", finalOrdered[29].get("city").?.text);
|
||||||
|
try std.testing.expectEqual(@as(i64, 3), finalOrdered[29].get(querymod.pkField).?.int);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `table_join` shares the exact same underlying-sstable-reshaping hazard as
|
||||||
|
// `table_select` above (see that test's header comment) - `flush`/`compact`
|
||||||
|
// swap `Db.sstables` out from under any in-flight reader while
|
||||||
|
// `Server.dispatch`'s `self.mutex` is what keeps a join's two-table scan
|
||||||
|
// from tearing. Ten customers / ten orders, deliberately NOT 1:1: two
|
||||||
|
// customers (9, 10) never appear in `orders`, and two orders (9, 10) carry a
|
||||||
|
// `customer_id` nobody has, so inner/left/right/full each land on a
|
||||||
|
// different, independently-checkable pair count instead of degenerating to
|
||||||
|
// the same number.
|
||||||
|
fn seedJoinConcurrencyTables(allocator: Allocator, db: *Db) !void {
|
||||||
|
var customers = try querymod.Table.open(db, allocator, "customers");
|
||||||
|
defer customers.deinit();
|
||||||
|
var pk: i64 = 1;
|
||||||
|
while (pk <= 10) : (pk += 1) {
|
||||||
|
var buf: [16]u8 = undefined;
|
||||||
|
const name = try std.fmt.bufPrint(&buf, "cust{d}", .{pk});
|
||||||
|
try customers.put(.{ .int = pk }, &.{.{ .name = "name", .value = .{ .text = name } }});
|
||||||
|
}
|
||||||
|
|
||||||
|
var orders = try querymod.Table.open(db, allocator, "orders");
|
||||||
|
defer orders.deinit();
|
||||||
|
pk = 1;
|
||||||
|
while (pk <= 8) : (pk += 1) {
|
||||||
|
try orders.put(.{ .int = pk }, &.{
|
||||||
|
.{ .name = "customer_id", .value = .{ .int = pk } },
|
||||||
|
.{ .name = "total", .value = .{ .int = pk * 10 } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try orders.put(.{ .int = 9 }, &.{
|
||||||
|
.{ .name = "customer_id", .value = .{ .int = 999 } },
|
||||||
|
.{ .name = "total", .value = .{ .int = 900 } },
|
||||||
|
});
|
||||||
|
try orders.put(.{ .int = 10 }, &.{
|
||||||
|
.{ .name = "customer_id", .value = .{ .int = 998 } },
|
||||||
|
.{ .name = "total", .value = .{ .int = 800 } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test "table_join 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);
|
||||||
|
|
||||||
Reference in new issue
Block a user