feat: add IncrWorker and run to concurrent
This commit is contained in:
1 parent
14dfdcf57a
commit
d8ee5f3e03
1 file changed
+157
@@ -155,3 +155,160 @@ test "incrBy on a fresh key starts from zero and returns the running total" {
|
||||
try std.testing.expectEqual(@as(i64, 103), try guarded.incrBy(allocator, "ctr", 100));
|
||||
}
|
||||
|
||||
const IncrWorker = struct {
|
||||
guarded: *GuardedDb,
|
||||
key: []const u8,
|
||||
iterations: usize,
|
||||
|
||||
fn run(self: IncrWorker) void {
|
||||
const allocator = std.heap.page_allocator;
|
||||
var i: usize = 0;
|
||||
while (i < self.iterations) : (i += 1) {
|
||||
_ = self.guarded.incrBy(allocator, self.key, 1) catch |err| {
|
||||
std.debug.panic("incrBy failed under real thread contention: {s}", .{@errorName(err)});
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "incrBy survives real concurrent OS threads with zero lost updates" {
|
||||
const allocator = std.testing.allocator;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTemp(&tmp, allocator);
|
||||
defer db.close();
|
||||
var guarded = GuardedDb.init(&db);
|
||||
|
||||
const threadCount = 6;
|
||||
const perThread = 300;
|
||||
var threads: [threadCount]std.Thread = undefined;
|
||||
for (&threads) |*t| {
|
||||
t.* = try std.Thread.spawn(.{}, IncrWorker.run, .{IncrWorker{
|
||||
.guarded = &guarded,
|
||||
.key = "shared-counter",
|
||||
.iterations = perThread,
|
||||
}});
|
||||
}
|
||||
for (threads) |t| t.join();
|
||||
|
||||
const raw = try guarded.get(allocator, "shared-counter");
|
||||
defer allocator.free(raw.?);
|
||||
const total = std.mem.readInt(i64, raw.?[0..8], .little);
|
||||
// A bare unsynchronized `db.get` + `db.put` read-modify-write would
|
||||
// routinely lose updates here (two threads read the same stale value,
|
||||
// both write back the same next value, one increment vanishes). The
|
||||
// mutex inside GuardedDb makes every incrBy fully serialized, so the
|
||||
// total below must land exactly on threadCount*perThread every single
|
||||
// run - not "usually", not "close to" - or the guard has a real bug.
|
||||
try std.testing.expectEqual(@as(i64, threadCount * perThread), total);
|
||||
}
|
||||
|
||||
const TransferWorker = struct {
|
||||
guarded: *GuardedDb,
|
||||
rounds: usize,
|
||||
|
||||
fn run(self: TransferWorker) void {
|
||||
const allocator = std.heap.page_allocator;
|
||||
var i: usize = 0;
|
||||
while (i < self.rounds) : (i += 1) {
|
||||
const g = self.guarded.acquire();
|
||||
defer g.release();
|
||||
const aRaw = g.db.get(allocator, "balanceA") catch @panic("get a failed");
|
||||
defer allocator.free(aRaw.?);
|
||||
const bRaw = g.db.get(allocator, "balanceB") catch @panic("get b failed");
|
||||
defer allocator.free(bRaw.?);
|
||||
const a = std.mem.readInt(i64, aRaw.?[0..8], .little);
|
||||
const b = std.mem.readInt(i64, bRaw.?[0..8], .little);
|
||||
if (a < 1) continue;
|
||||
var aBuf: [8]u8 = undefined;
|
||||
var bBuf: [8]u8 = undefined;
|
||||
std.mem.writeInt(i64, &aBuf, a - 1, .little);
|
||||
std.mem.writeInt(i64, &bBuf, b + 1, .little);
|
||||
g.db.put("balanceA", &aBuf) catch @panic("put a failed");
|
||||
g.db.put("balanceB", &bBuf) catch @panic("put b failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const InvariantReader = struct {
|
||||
guarded: *GuardedDb,
|
||||
checks: usize,
|
||||
violations: *std.atomic.Value(usize),
|
||||
|
||||
fn run(self: InvariantReader) void {
|
||||
const allocator = std.heap.page_allocator;
|
||||
var i: usize = 0;
|
||||
while (i < self.checks) : (i += 1) {
|
||||
const g = self.guarded.acquire();
|
||||
const aRaw = g.db.get(allocator, "balanceA") catch @panic("get a failed");
|
||||
const bRaw = g.db.get(allocator, "balanceB") catch @panic("get b failed");
|
||||
const a = std.mem.readInt(i64, aRaw.?[0..8], .little);
|
||||
const b = std.mem.readInt(i64, bRaw.?[0..8], .little);
|
||||
g.release();
|
||||
allocator.free(aRaw.?);
|
||||
allocator.free(bRaw.?);
|
||||
if (a + b != 1000) _ = self.violations.fetchAdd(1, .monotonic);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "acquire lets a writer move value between two keys atomically under a concurrent reader" {
|
||||
const allocator = std.testing.allocator;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTemp(&tmp, allocator);
|
||||
defer db.close();
|
||||
var guarded = GuardedDb.init(&db);
|
||||
|
||||
var initA: [8]u8 = undefined;
|
||||
var initB: [8]u8 = undefined;
|
||||
std.mem.writeInt(i64, &initA, 1000, .little);
|
||||
std.mem.writeInt(i64, &initB, 0, .little);
|
||||
try guarded.put("balanceA", &initA);
|
||||
try guarded.put("balanceB", &initB);
|
||||
|
||||
var violations = std.atomic.Value(usize).init(0);
|
||||
const writerThread = try std.Thread.spawn(.{}, TransferWorker.run, .{TransferWorker{
|
||||
.guarded = &guarded,
|
||||
.rounds = 1000,
|
||||
}});
|
||||
const readerThread = try std.Thread.spawn(.{}, InvariantReader.run, .{InvariantReader{
|
||||
.guarded = &guarded,
|
||||
.checks = 1000,
|
||||
.violations = &violations,
|
||||
}});
|
||||
writerThread.join();
|
||||
readerThread.join();
|
||||
|
||||
// Every single reader observation is required to see the invariant
|
||||
// balanceA + balanceB == 1000 hold, because each transfer's two puts
|
||||
// happen inside one `acquire()`/`release()` pair the reader's own
|
||||
// acquire() must wait out entirely - a real race would show up here
|
||||
// as a nonzero violation count, not as a crash, which is why this
|
||||
// asserts the counter is exactly zero instead of merely "did not hang".
|
||||
try std.testing.expectEqual(@as(usize, 0), violations.load(.monotonic));
|
||||
|
||||
const finalARaw = try guarded.get(allocator, "balanceA");
|
||||
defer allocator.free(finalARaw.?);
|
||||
const finalBRaw = try guarded.get(allocator, "balanceB");
|
||||
defer allocator.free(finalBRaw.?);
|
||||
const finalA = std.mem.readInt(i64, finalARaw.?[0..8], .little);
|
||||
const finalB = std.mem.readInt(i64, finalBRaw.?[0..8], .little);
|
||||
try std.testing.expectEqual(@as(i64, 1000), finalA + finalB);
|
||||
try std.testing.expectEqual(@as(i64, 0), finalA);
|
||||
try std.testing.expectEqual(@as(i64, 1000), finalB);
|
||||
}
|
||||
|
||||
test "apply is usable concurrently: disjoint batches from different threads never clobber each other" {
|
||||
const allocator = std.testing.allocator;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTemp(&tmp, allocator);
|
||||
defer db.close();
|
||||
var guarded = GuardedDb.init(&db);
|
||||
|
||||
const ApplyWorker = struct {
|
||||
guarded: *GuardedDb,
|
||||
prefix: u8,
|
||||
count: usize,
|
||||
|
||||
Reference in new issue
Block a user