db: replace engine mutex with writer-preferring RwLock, add lock_read/unlock_read

This commit is contained in:
mongo-light
2026-08-02 10:29:33 +03:00
parent 62a4c4244c
commit b71b97824e

View File

@@ -1,7 +1,9 @@
//! In-memory database engine backed by the append-only log. Maps //! In-memory database engine backed by the append-only log. Maps
//! db -> collection -> _id(serialized) -> owned Document. All mutations are //! db -> collection -> _id(serialized) -> owned Document. All mutations are
//! logged and synced before they become visible in memory, so a crash never //! logged and synced before they become visible in memory, so a crash never
//! loses a committed write. Callers must hold `mutex` around a command. //! loses a committed write. Callers must hold the write lock (`lock`) around
//! any command that mutates state, and the read lock (`lock_read`) around
//! read-only commands so reads overlap with each other.
const std = @import("std"); const std = @import("std");
const bson = @import("bson.zig"); const bson = @import("bson.zig");
@@ -22,7 +24,10 @@ pub const Db = struct {
pub const Engine = struct { pub const Engine = struct {
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
io: std.Io, io: std.Io,
mutex: std.Io.Mutex, // One writer at a time (log append + fsync, map mutation); many
// concurrent readers (find/count/aggregate scans). Writer-preferring:
// a queued writer blocks new readers rather than starving.
rwlock: std.Io.RwLock,
log: storage.Log, log: storage.Log,
dbs: std.StringHashMapUnmanaged(Db), dbs: std.StringHashMapUnmanaged(Db),
seq: u64, seq: u64,
@@ -32,7 +37,7 @@ pub const Engine = struct {
var engine = Engine{ var engine = Engine{
.gpa = gpa, .gpa = gpa,
.io = io, .io = io,
.mutex = std.Io.Mutex.init, .rwlock = .init,
.log = try storage.Log.open(gpa, io, path), .log = try storage.Log.open(gpa, io, path),
.dbs = .empty, .dbs = .empty,
.seq = 0, .seq = 0,
@@ -68,14 +73,25 @@ pub const Engine = struct {
self.log.close(); self.log.close();
} }
// -- commands (callers must hold the mutex) ------------------------------ // -- commands (callers must hold the matching lock) ---------------------
/// Exclusive lock: for commands that mutate the engine.
pub fn lock(self: *Engine) !void { pub fn lock(self: *Engine) !void {
try self.mutex.lock(self.io); try self.rwlock.lock(self.io);
} }
pub fn unlock(self: *Engine) void { pub fn unlock(self: *Engine) void {
self.mutex.unlock(self.io); self.rwlock.unlock(self.io);
}
/// Shared lock: for read-only commands (find, count, aggregate, list*).
/// Multiple readers may hold it simultaneously; writers wait for them.
pub fn lock_read(self: *Engine) !void {
try self.rwlock.lockShared(self.io);
}
pub fn unlock_read(self: *Engine) void {
self.rwlock.unlockShared(self.io);
} }
/// Insert a document. Fails with error.DuplicateKey if the _id exists. /// Insert a document. Fails with error.DuplicateKey if the _id exists.
@@ -247,7 +263,7 @@ pub const Engine = struct {
} }
/// Rewrite the log with only live documents, atomically swapping the file. /// Rewrite the log with only live documents, atomically swapping the file.
/// Callers must hold the mutex. /// Callers must hold the write lock.
pub fn compact(self: *Engine) !void { pub fn compact(self: *Engine) !void {
const tmp_path = try std.fmt.allocPrint(self.gpa, "{s}.tmp", .{self.log.path}); const tmp_path = try std.fmt.allocPrint(self.gpa, "{s}.tmp", .{self.log.path});
defer self.gpa.free(tmp_path); defer self.gpa.free(tmp_path);