rename project to MultiforaDB

Prose and benchmark tables use MultiforaDB; the binary, the CLI usage
line, the log-message prefix and the default database file use
multiforadb.

Two consequences worth noting:

- build.zig.zon's fingerprint is derived from the package name, so it
  had to change with it (Zig refuses to build otherwise). A consumer
  pinning this package by fingerprint needs updating.
- the default --db path is now multiforadb.log, and getCmdLineOpts
  reports it as dbpath. An existing mongo-lite.log has to be passed
  explicitly with --db.

The e2e harness abbreviated the old name as ML_; that is now MFDB_,
including the documented ML_BIN override (MFDB_BIN) and the scratch
file names. MD_ (mongod) is untouched.

compare-run.sh spawned the server by absolute path under a
sandbox/mongo-lite directory that no longer exists; that block already
runs from tests/e2e, so it uses a relative path now.

The archived reports under tests/e2e/results/ keep the old name: they
record what the old binary measured.
This commit is contained in:
2026-08-03 12:35:01 +03:00
parent ac464f2b92
commit d4c9b04f21
20 changed files with 129 additions and 129 deletions

View File

@@ -1,4 +1,4 @@
# mongo-lite # MultiforaDB
A lightweight, embedded MongoDB-compatible document database written in A lightweight, embedded MongoDB-compatible document database written in
Zig 0.16. Like SQLite, it stores everything in a single file; unlike SQLite, Zig 0.16. Like SQLite, it stores everything in a single file; unlike SQLite,
@@ -11,7 +11,7 @@ driver, PyMongo — connect over TCP and just work.
zig build # build the server zig build # build the server
zig build test # run the unit test suite zig build test # run the unit test suite
zig-out/bin/mongo-lite --port 27017 --db data.log --compact-threshold 256m zig-out/bin/multiforadb --port 27017 --db data.log --compact-threshold 256m
# in another terminal: # in another terminal:
mongosh --port 27017 mongosh --port 27017
@@ -181,27 +181,27 @@ the `tests/e2e/big.js` harness (12-core/32 GB Mac):
## Performance vs MongoDB ## Performance vs MongoDB
`tests/e2e/compare-run.sh` runs the same driver workload (1 GB, 65,536 × `tests/e2e/compare-run.sh` runs the same driver workload (1 GB, 65,536 ×
16 KB docs, every write durable — mongo-lite fsyncs per command, mongod 16 KB docs, every write durable — MultiforaDB fsyncs per command, mongod
runs with `j: true`) against each server and prints a side-by-side table. runs with `j: true`) against each server and prints a side-by-side table.
With the ReleaseFast default build (MongoDB 8.3.7 on the same Mac): With the ReleaseFast default build (MongoDB 8.3.7 on the same Mac):
| benchmark | mongo-lite | mongodb | winner | | benchmark | MultiforaDB | mongodb | winner |
|---|---|---|---| |---|---|---|---|
| insertOne (sequential) | 0.20 ms | 4.7 ms | **mongo-lite ×24** | | insertOne (sequential) | 0.20 ms | 4.7 ms | **MultiforaDB ×24** |
| bulk insert (insertMany) | 752 MB/s | 744 MB/s | mongo-lite | | bulk insert (insertMany) | 752 MB/s | 744 MB/s | MultiforaDB |
| createIndex({k: 1}) | 67 ms | 76 ms | **mongo-lite** | | createIndex({k: 1}) | 67 ms | 76 ms | **MultiforaDB** |
| countDocuments({}) | 2.6 ms | 11.2 ms | **mongo-lite ×4** | | countDocuments({}) | 2.6 ms | 11.2 ms | **MultiforaDB ×4** |
| findOne({_id}) | 0.45 ms | 0.65 ms | **mongo-lite** | | findOne({_id}) | 0.45 ms | 0.65 ms | **MultiforaDB** |
| findOne indexed | 0.54 ms | 4.6 ms | **mongo-lite ×8** | | findOne indexed | 0.54 ms | 4.6 ms | **MultiforaDB ×8** |
| range-scan count | 13.7 ms | 12.6 ms | mongodb ×1.1 | | range-scan count | 13.7 ms | 12.6 ms | mongodb ×1.1 |
| sort + limit(20), on `_id` | 2.3 ms | 2.0 ms | mongodb ×1.1 | | sort + limit(20), on `_id` | 2.3 ms | 2.0 ms | mongodb ×1.1 |
| sort + limit(20), indexed field | 1.0 ms | — | — | | sort + limit(20), indexed field | 1.0 ms | — | — |
| aggregate $group | 8.1 ms | 12.3 ms | **mongo-lite** | | aggregate $group | 8.1 ms | 12.3 ms | **MultiforaDB** |
| updateOne({_id}) | 0.15 ms | 0.19 ms | **mongo-lite** | | updateOne({_id}) | 0.15 ms | 0.19 ms | **MultiforaDB** |
| updateMany (65 docs) | 1.7 ms | 6.1 ms | **mongo-lite ×3.6** | | updateMany (65 docs) | 1.7 ms | 6.1 ms | **MultiforaDB ×3.6** |
| deleteOne + insert | 0.50 ms | 4.9 ms | **mongo-lite ×10** | | deleteOne + insert | 0.50 ms | 4.9 ms | **MultiforaDB ×10** |
| server RSS | 539 MB | 1.3 GB | **mongo-lite ×2.4** | | server RSS | 539 MB | 1.3 GB | **MultiforaDB ×2.4** |
| kill -9 → reopen | 0.8 s | 1.3 s | **mongo-lite** | | kill -9 → reopen | 0.8 s | 1.3 s | **MultiforaDB** |
| db on disk | 97 MB | 91 MB | mongodb | | db on disk | 97 MB | 91 MB | mongodb |
The engine now holds every document as canonical BSON bytes in a The engine now holds every document as canonical BSON bytes in a

View File

@@ -23,7 +23,7 @@ pub fn build(b: *std.Build) void {
}); });
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "mongo-lite", .name = "multiforadb",
.root_module = exe_mod, .root_module = exe_mod,
}); });
b.installArtifact(exe); b.installArtifact(exe);
@@ -31,7 +31,7 @@ pub fn build(b: *std.Build) void {
const run_cmd = b.addRunArtifact(exe); const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep()); run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args); if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Run mongo-lite server"); const run_step = b.step("run", "Run multiforadb server");
run_step.dependOn(&run_cmd.step); run_step.dependOn(&run_cmd.step);
const test_mod = b.createModule(.{ const test_mod = b.createModule(.{

View File

@@ -1,7 +1,7 @@
.{ .{
.name = .mongo_lite, .name = .multiforadb,
.version = "0.0.1", .version = "0.0.1",
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.paths = .{""}, .paths = .{""},
.fingerprint = 0xcca51d85af632c24, .fingerprint = 0x8305abefdc9db3e3,
} }

View File

@@ -17,14 +17,14 @@ const std = @import("std");
/// Panic unless `ok`. Active in every optimize mode; see the module comment. /// Panic unless `ok`. Active in every optimize mode; see the module comment.
pub fn assert(ok: bool) void { pub fn assert(ok: bool) void {
if (!ok) @panic("mongo-lite: assertion failed"); if (!ok) @panic("multiforadb: assertion failed");
} }
/// Panic unless `ok`, naming the invariant that broke. Prefer this where the /// Panic unless `ok`, naming the invariant that broke. Prefer this where the
/// condition alone does not say what went wrong -- the message lands in the /// condition alone does not say what went wrong -- the message lands in the
/// crash output, which may be all an operator has to go on. /// crash output, which may be all an operator has to go on.
pub fn assert_msg(ok: bool, comptime message: []const u8) void { pub fn assert_msg(ok: bool, comptime message: []const u8) void {
if (!ok) @panic("mongo-lite: assertion failed: " ++ message); if (!ok) @panic("multiforadb: assertion failed: " ++ message);
} }
test "assert passes on true and is callable in every mode" { test "assert passes on true and is callable in every mode" {

View File

@@ -170,7 +170,7 @@ pub fn dispatch(ctx: *Context, msg: *wire.Message, reply: *wire.Reply) !void {
// rather than turning an applied write into an error the client retries. // rather than turning an applied write into an error the client retries.
if (ctx.engine.take_compact()) { if (ctx.engine.take_compact()) {
ctx.engine.compact() catch |err| { ctx.engine.compact() catch |err| {
std.debug.print("mongo-lite: compaction failed: {s}\n", .{@errorName(err)}); std.debug.print("multiforadb: compaction failed: {s}\n", .{@errorName(err)});
ctx.engine.request_compact(); ctx.engine.request_compact();
}; };
} }
@@ -226,7 +226,7 @@ fn cmd_ping(_: *Context, _: *wire.Message, reply: *wire.Reply) !void {
fn cmd_build_info(_: *Context, _: *wire.Message, reply: *wire.Reply) !void { fn cmd_build_info(_: *Context, _: *wire.Message, reply: *wire.Reply) !void {
try reply.put("version", .{ .string = "4.4.0" }); try reply.put("version", .{ .string = "4.4.0" });
try reply.put("gitVersion", .{ .string = "mongo-lite" }); try reply.put("gitVersion", .{ .string = "multiforadb" });
try reply.put("versionArray", .{ .array = try int_array(reply, &.{ 4, 4, 0, 0 }) }); try reply.put("versionArray", .{ .array = try int_array(reply, &.{ 4, 4, 0, 0 }) });
try reply.put("openssl", .{ .doc = &.{} }); try reply.put("openssl", .{ .doc = &.{} });
try reply.put("loaderFlags", .{ .string = "" }); try reply.put("loaderFlags", .{ .string = "" });
@@ -283,7 +283,7 @@ fn cmd_host_info(ctx: *Context, _: *wire.Message, reply: *wire.Reply) !void {
fn cmd_get_cmd_line_opts(_: *Context, _: *wire.Message, reply: *wire.Reply) !void { fn cmd_get_cmd_line_opts(_: *Context, _: *wire.Message, reply: *wire.Reply) !void {
const argv = try reply.arena_alloc().alloc(bson.Pair, 2); const argv = try reply.arena_alloc().alloc(bson.Pair, 2);
argv[0] = .{ .key = "dbpath", .value = .{ .string = "mongo-lite.log" } }; argv[0] = .{ .key = "dbpath", .value = .{ .string = "multiforadb.log" } };
argv[1] = .{ .key = "port", .value = .{ .int32 = 27017 } }; argv[1] = .{ .key = "port", .value = .{ .int32 = 27017 } };
try reply.put("argv", .{ .array = &.{} }); try reply.put("argv", .{ .array = &.{} });
try reply.put("parsed", .{ .doc = argv }); try reply.put("parsed", .{ .doc = argv });

View File

@@ -986,7 +986,7 @@ pub const Engine = struct {
// epilogue called us. // epilogue called us.
self.request_compact(); self.request_compact();
std.debug.print( std.debug.print(
"mongo-lite: compaction gave up after {d} attempts (concurrent writes); will retry\n", "multiforadb: compaction gave up after {d} attempts (concurrent writes); will retry\n",
.{attempt_max}, .{attempt_max},
); );
} }
@@ -1045,7 +1045,7 @@ pub const Engine = struct {
while (doc_it.next()) |doc_entry| { while (doc_it.next()) |doc_entry| {
ix.append_doc_entries(self.gpa, coll.doc_bytes(doc_entry.value_ptr.*), doc_entry.key_ptr.*) catch |err| switch (err) { ix.append_doc_entries(self.gpa, coll.doc_bytes(doc_entry.value_ptr.*), doc_entry.key_ptr.*) catch |err| switch (err) {
error.ParallelArrays => { error.ParallelArrays => {
std.debug.print("mongo-lite: WARNING: index '{s}' cannot index an existing document; entry skipped\n", .{ix.name}); std.debug.print("multiforadb: WARNING: index '{s}' cannot index an existing document; entry skipped\n", .{ix.name});
continue; continue;
}, },
else => return err, else => return err,
@@ -1053,7 +1053,7 @@ pub const Engine = struct {
} }
// Tolerated, not enforced: the database must always open. // Tolerated, not enforced: the database must always open.
if (try ix.finish_bulk(self.gpa, false)) { if (try ix.finish_bulk(self.gpa, false)) {
std.debug.print("mongo-lite: WARNING: unique index '{s}' has duplicate keys in existing data; duplicates not enforced for existing documents\n", .{ix.name}); std.debug.print("multiforadb: WARNING: unique index '{s}' has duplicate keys in existing data; duplicates not enforced for existing documents\n", .{ix.name});
} }
} }
@@ -1103,7 +1103,7 @@ fn apply_record(ctx: *anyopaque, record: storage.Record, doc: *bson.Document) an
switch (record.type) { switch (record.type) {
storage.record_type_index_create => { storage.record_type_index_create => {
self.register_index_from_spec(coll, doc) catch |err| { self.register_index_from_spec(coll, doc) catch |err| {
std.debug.print("mongo-lite: index create record failed to apply: {s}\n", .{@errorName(err)}); std.debug.print("multiforadb: index create record failed to apply: {s}\n", .{@errorName(err)});
return; return;
}; };
return; return;
@@ -1121,7 +1121,7 @@ fn apply_record(ctx: *anyopaque, record: storage.Record, doc: *bson.Document) an
} }
const id_value = doc.get("_id") orelse { const id_value = doc.get("_id") orelse {
std.debug.print("mongo-lite: log record without _id, skipping\n", .{}); std.debug.print("multiforadb: log record without _id, skipping\n", .{});
return; return;
}; };
const id_key = try bson.serialize_value(self.gpa, id_value); const id_key = try bson.serialize_value(self.gpa, id_value);

View File

@@ -1,4 +1,4 @@
// mongo-lite core library. Public entry point for tests and the server. // multiforadb core library. Public entry point for tests and the server.
pub const assert = @import("assert.zig"); pub const assert = @import("assert.zig");
pub const bson = @import("bson.zig"); pub const bson = @import("bson.zig");

View File

@@ -2,12 +2,12 @@ const std = @import("std");
const mongo = @import("mongo"); const mongo = @import("mongo");
const usage = const usage =
\\mongo-lite — lightweight MongoDB-compatible document database \\MultiforaDB — lightweight MongoDB-compatible document database
\\ \\
\\usage: mongo-lite [options] \\usage: multiforadb [options]
\\ --port <n> listen port (default 27017) \\ --port <n> listen port (default 27017)
\\ --bind <ip> bind address (default 127.0.0.1) \\ --bind <ip> bind address (default 127.0.0.1)
\\ --db <path> database file (default mongo-lite.log) \\ --db <path> database file (default multiforadb.log)
\\ --ttl-sweep-secs <n> \\ --ttl-sweep-secs <n>
\\ seconds between TTL index sweeps (default 60, 0 disables) \\ seconds between TTL index sweeps (default 60, 0 disables)
\\ --compact-threshold <bytes> \\ --compact-threshold <bytes>
@@ -47,7 +47,7 @@ fn parse_size_suffix(v: []const u8) ?u64 {
pub fn main(init: std.process.Init) !void { pub fn main(init: std.process.Init) !void {
var port: u16 = 27017; var port: u16 = 27017;
var bind_ip: []const u8 = "127.0.0.1"; var bind_ip: []const u8 = "127.0.0.1";
var db_path: []const u8 = "mongo-lite.log"; var db_path: []const u8 = "multiforadb.log";
var ttl_sweep_secs: i64 = 60; var ttl_sweep_secs: i64 = 60;
var compact_threshold: u64 = 16 * 1024 * 1024; var compact_threshold: u64 = 16 * 1024 * 1024;
@@ -58,7 +58,7 @@ pub fn main(init: std.process.Init) !void {
if (std.mem.eql(u8, arg, "--port")) { if (std.mem.eql(u8, arg, "--port")) {
const v = it.next() orelse return error.MissingValue; const v = it.next() orelse return error.MissingValue;
port = std.fmt.parseInt(u16, v, 10) catch { port = std.fmt.parseInt(u16, v, 10) catch {
std.debug.print("mongo-lite: invalid port '{s}'\n", .{v}); std.debug.print("multiforadb: invalid port '{s}'\n", .{v});
return error.InvalidPort; return error.InvalidPort;
}; };
} else if (std.mem.eql(u8, arg, "--bind")) { } else if (std.mem.eql(u8, arg, "--bind")) {
@@ -72,17 +72,17 @@ pub fn main(init: std.process.Init) !void {
// only thing parseInt would otherwise let through. // only thing parseInt would otherwise let through.
ttl_sweep_secs = std.fmt.parseInt(i64, v, 10) catch -1; ttl_sweep_secs = std.fmt.parseInt(i64, v, 10) catch -1;
if (ttl_sweep_secs < 0) { if (ttl_sweep_secs < 0) {
std.debug.print("mongo-lite: invalid ttl sweep interval '{s}'\n", .{v}); std.debug.print("multiforadb: invalid ttl sweep interval '{s}'\n", .{v});
return error.InvalidTtlSweepSecs; return error.InvalidTtlSweepSecs;
} }
} else if (std.mem.eql(u8, arg, "--compact-threshold")) { } else if (std.mem.eql(u8, arg, "--compact-threshold")) {
const v = it.next() orelse return error.MissingValue; const v = it.next() orelse return error.MissingValue;
const parsed = parse_size_suffix(v) orelse { const parsed = parse_size_suffix(v) orelse {
std.debug.print("mongo-lite: invalid compact threshold '{s}'\n", .{v}); std.debug.print("multiforadb: invalid compact threshold '{s}'\n", .{v});
return error.InvalidCompactThreshold; return error.InvalidCompactThreshold;
}; };
if (parsed < 1024 * 1024) { if (parsed < 1024 * 1024) {
std.debug.print("mongo-lite: compact threshold must be at least 1m\n", .{}); std.debug.print("multiforadb: compact threshold must be at least 1m\n", .{});
return error.InvalidCompactThreshold; return error.InvalidCompactThreshold;
} }
compact_threshold = parsed; compact_threshold = parsed;
@@ -90,7 +90,7 @@ pub fn main(init: std.process.Init) !void {
try std.Io.File.writeStreamingAll(.stdout(), init.io, usage); try std.Io.File.writeStreamingAll(.stdout(), init.io, usage);
return; return;
} else { } else {
std.debug.print("mongo-lite: unknown option '{s}'\n{s}", .{ arg, usage }); std.debug.print("multiforadb: unknown option '{s}'\n{s}", .{ arg, usage });
return error.UnknownOption; return error.UnknownOption;
} }
} }
@@ -99,7 +99,7 @@ pub fn main(init: std.process.Init) !void {
var engine = try mongo.db.Engine.open(init.gpa, init.io, db_path); var engine = try mongo.db.Engine.open(init.gpa, init.io, db_path);
defer engine.deinit(); defer engine.deinit();
engine.compact_threshold = compact_threshold; engine.compact_threshold = compact_threshold;
std.debug.print("mongo-lite: opened database '{s}' (compact threshold {d})\n", .{ db_path, compact_threshold }); std.debug.print("multiforadb: opened database '{s}' (compact threshold {d})\n", .{ db_path, compact_threshold });
var server = mongo.server.Server{ var server = mongo.server.Server{
.gpa = init.gpa, .gpa = init.gpa,

View File

@@ -35,7 +35,7 @@ pub const Server = struct {
var listener = try addr.listen(io, .{ .reuse_address = true }); var listener = try addr.listen(io, .{ .reuse_address = true });
defer listener.deinit(io); defer listener.deinit(io);
std.debug.print("mongo-lite: listening on {s}:{d}\n", .{ self.bind_ip, self.port }); std.debug.print("multiforadb: listening on {s}:{d}\n", .{ self.bind_ip, self.port });
var group: std.Io.Group = .init; var group: std.Io.Group = .init;
defer group.cancel(io); defer group.cancel(io);
@@ -48,7 +48,7 @@ pub const Server = struct {
const stream = listener.accept(io) catch |err| switch (err) { const stream = listener.accept(io) catch |err| switch (err) {
error.Canceled => return, error.Canceled => return,
else => { else => {
std.debug.print("mongo-lite: accept error: {s}\n", .{@errorName(err)}); std.debug.print("multiforadb: accept error: {s}\n", .{@errorName(err)});
continue; continue;
}, },
}; };
@@ -70,12 +70,12 @@ fn ttl_monitor(io: std.Io, server: *Server) error{Canceled}!void {
try std.Io.sleep(io, interval, .awake); try std.Io.sleep(io, interval, .awake);
const now_ms = std.Io.Timestamp.now(io, .real).toMilliseconds(); const now_ms = std.Io.Timestamp.now(io, .real).toMilliseconds();
_ = server.engine.ttl_sweep(now_ms) catch |err| { _ = server.engine.ttl_sweep(now_ms) catch |err| {
std.debug.print("mongo-lite: TTL sweep failed: {s}\n", .{@errorName(err)}); std.debug.print("multiforadb: TTL sweep failed: {s}\n", .{@errorName(err)});
continue; continue;
}; };
if (server.engine.take_compact()) { if (server.engine.take_compact()) {
server.engine.compact() catch |err| { server.engine.compact() catch |err| {
std.debug.print("mongo-lite: compaction failed: {s}\n", .{@errorName(err)}); std.debug.print("multiforadb: compaction failed: {s}\n", .{@errorName(err)});
}; };
} }
} }
@@ -120,7 +120,7 @@ fn handle_connection_inner(io: std.Io, stream: std.Io.net.Stream, server: *Serve
reader.interface.readSliceAll(&len_bytes) catch return; // clean client disconnect (EOF or RST) reader.interface.readSliceAll(&len_bytes) catch return; // clean client disconnect (EOF or RST)
const total: u32 = std.mem.readInt(u32, &len_bytes, .little); const total: u32 = std.mem.readInt(u32, &len_bytes, .little);
if (total < 16 or total > wire.max_message_size) { if (total < 16 or total > wire.max_message_size) {
std.debug.print("mongo-lite: bad message length {d} on conn {d}\n", .{ total, connection_id }); std.debug.print("multiforadb: bad message length {d} on conn {d}\n", .{ total, connection_id });
return; return;
} }
@@ -129,14 +129,14 @@ fn handle_connection_inner(io: std.Io, stream: std.Io.net.Stream, server: *Serve
msg_buf.items.len = total; msg_buf.items.len = total;
std.mem.writeInt(u32, msg_buf.items[0..4], total, .little); std.mem.writeInt(u32, msg_buf.items[0..4], total, .little);
reader.interface.readSliceAll(msg_buf.items[4..]) catch |err| { reader.interface.readSliceAll(msg_buf.items[4..]) catch |err| {
std.debug.print("mongo-lite: read error on conn {d}: {s} (body, len {d})\n", .{ connection_id, @errorName(err), total }); std.debug.print("multiforadb: read error on conn {d}: {s} (body, len {d})\n", .{ connection_id, @errorName(err), total });
return; return;
}; };
var msg = wire.Message.parse(server.gpa, msg_buf.items) catch |err| { var msg = wire.Message.parse(server.gpa, msg_buf.items) catch |err| {
// Unparseable request: close the connection. // Unparseable request: close the connection.
const op: i32 = if (msg_buf.items.len >= 16) std.mem.readInt(i32, msg_buf.items[12..16], .little) else 0; const op: i32 = if (msg_buf.items.len >= 16) std.mem.readInt(i32, msg_buf.items[12..16], .little) else 0;
std.debug.print("mongo-lite: bad message on conn {d}: {s} (opCode {d})\n", .{ connection_id, @errorName(err), op }); std.debug.print("multiforadb: bad message on conn {d}: {s} (opCode {d})\n", .{ connection_id, @errorName(err), op });
return; return;
}; };
defer msg.deinit(); defer msg.deinit();
@@ -146,7 +146,7 @@ fn handle_connection_inner(io: std.Io, stream: std.Io.net.Stream, server: *Serve
commands.dispatch(&ctx, &msg, &reply) catch |err| { commands.dispatch(&ctx, &msg, &reply) catch |err| {
// Discard any partial reply (the client would read the first // Discard any partial reply (the client would read the first
// ok field, which may already say 1) and send a clean error. // ok field, which may already say 1) and send a clean error.
std.debug.print("mongo-lite: dispatch error on conn {d} cmd {s}: {s}\n", .{ connection_id, msg.command_name(), @errorName(err) }); std.debug.print("multiforadb: dispatch error on conn {d} cmd {s}: {s}\n", .{ connection_id, msg.command_name(), @errorName(err) });
reply.pairs.clearRetainingCapacity(); reply.pairs.clearRetainingCapacity();
reply.put_error( reply.put_error(
@intFromEnum(commands.ErrorCode.internal_error), @intFromEnum(commands.ErrorCode.internal_error),
@@ -161,16 +161,16 @@ fn handle_connection_inner(io: std.Io, stream: std.Io.net.Stream, server: *Serve
else else
reply.build(server.gpa, reply_request_id, msg.request_id, &out_buf); reply.build(server.gpa, reply_request_id, msg.request_id, &out_buf);
built catch |err| { built catch |err| {
std.debug.print("mongo-lite: reply build error on conn {d}: {s}\n", .{ connection_id, @errorName(err) }); std.debug.print("multiforadb: reply build error on conn {d}: {s}\n", .{ connection_id, @errorName(err) });
return; return;
}; };
reply_request_id +%= 1; reply_request_id +%= 1;
writer.interface.writeAll(out_buf.items) catch |err| { writer.interface.writeAll(out_buf.items) catch |err| {
std.debug.print("mongo-lite: write error on conn {d}: {s}\n", .{ connection_id, @errorName(err) }); std.debug.print("multiforadb: write error on conn {d}: {s}\n", .{ connection_id, @errorName(err) });
return; return;
}; };
writer.interface.flush() catch |err| { writer.interface.flush() catch |err| {
std.debug.print("mongo-lite: flush error on conn {d}: {s}\n", .{ connection_id, @errorName(err) }); std.debug.print("multiforadb: flush error on conn {d}: {s}\n", .{ connection_id, @errorName(err) });
return; return;
}; };
} }

View File

@@ -227,14 +227,14 @@ pub const Log = struct {
while (true) { while (true) {
var hdr: [block_header_len]u8 = undefined; var hdr: [block_header_len]u8 = undefined;
const n = self.file.readPositionalAll(self.io, &hdr, pos) catch |err| { const n = self.file.readPositionalAll(self.io, &hdr, pos) catch |err| {
std.debug.print("mongo-lite: log read error at {d}: {s}\n", .{ pos, @errorName(err) }); std.debug.print("multiforadb: log read error at {d}: {s}\n", .{ pos, @errorName(err) });
return error.InvalidLog; return error.InvalidLog;
}; };
if (n == 0) return; // clean end if (n == 0) return; // clean end
if (n < block_header_len) return; // torn tail: partial block header if (n < block_header_len) return; // torn tail: partial block header
const total: u32 = std.mem.readInt(u32, hdr[0..4], .little); const total: u32 = std.mem.readInt(u32, hdr[0..4], .little);
if (total < block_header_len or total - block_header_len > max_block_payload) { if (total < block_header_len or total - block_header_len > max_block_payload) {
std.debug.print("mongo-lite: corrupt block length {d} at {d}\n", .{ total, pos }); std.debug.print("multiforadb: corrupt block length {d} at {d}\n", .{ total, pos });
return; // torn tail: impossible length, nothing to validate return; // torn tail: impossible length, nothing to validate
} }
const payload_len: usize = total - block_header_len; const payload_len: usize = total - block_header_len;
@@ -262,7 +262,7 @@ pub const Log = struct {
codec_raw => try decomp.appendSlice(self.gpa, payload), codec_raw => try decomp.appendSlice(self.gpa, payload),
codec_lz4 => try lz4_decompress(self.gpa, payload, &decomp), codec_lz4 => try lz4_decompress(self.gpa, payload, &decomp),
else => { else => {
std.debug.print("mongo-lite: unknown block codec {d} at {d}\n", .{ codec, pos }); std.debug.print("multiforadb: unknown block codec {d} at {d}\n", .{ codec, pos });
return error.InvalidLog; return error.InvalidLog;
}, },
} }
@@ -284,12 +284,12 @@ pub const Log = struct {
if (bytes.len < 4) return error.InvalidLog; if (bytes.len < 4) return error.InvalidLog;
const total: u32 = std.mem.readInt(u32, bytes[0..4], .little); const total: u32 = std.mem.readInt(u32, bytes[0..4], .little);
if (total < header_len) { if (total < header_len) {
std.debug.print("mongo-lite: corrupt record length {d} at {d}\n", .{ total, pos }); std.debug.print("multiforadb: corrupt record length {d} at {d}\n", .{ total, pos });
return error.InvalidLog; return error.InvalidLog;
} }
const payload_len: usize = total - 4; const payload_len: usize = total - 4;
if (payload_len > max_record_payload) { if (payload_len > max_record_payload) {
std.debug.print("mongo-lite: record too large at {d}\n", .{pos}); std.debug.print("multiforadb: record too large at {d}\n", .{pos});
return error.InvalidLog; return error.InvalidLog;
} }
if (bytes.len < total) return error.InvalidLog; // record crosses the block end if (bytes.len < total) return error.InvalidLog; // record crosses the block end
@@ -309,7 +309,7 @@ pub const Log = struct {
const doc = try self.gpa.create(bson.Document); const doc = try self.gpa.create(bson.Document);
doc.* = bson.Document.parse(self.gpa, doc_bytes) catch { doc.* = bson.Document.parse(self.gpa, doc_bytes) catch {
self.gpa.destroy(doc); self.gpa.destroy(doc);
std.debug.print("mongo-lite: unparseable doc in log at {d}\n", .{pos}); std.debug.print("multiforadb: unparseable doc in log at {d}\n", .{pos});
return error.InvalidLog; return error.InvalidLog;
}; };
try callback(ctx, .{ try callback(ctx, .{

View File

@@ -1,6 +1,6 @@
# End-to-end tests with the official MongoDB Node.js driver # End-to-end tests with the official MongoDB Node.js driver
These exercise mongo-lite from a real driver over TCP: full CRUD, query These exercise MultiforaDB from a real driver over TCP: full CRUD, query
operators, aggregation, error codes, concurrent clients, crash recovery, and operators, aggregation, error codes, concurrent clients, crash recovery, and
the whole lifecycle including server restarts. the whole lifecycle including server restarts.
@@ -18,7 +18,7 @@ Most suites expect a server running on port 27020:
```sh ```sh
zig build zig build
zig-out/bin/mongo-lite --port 27020 --db /tmp/ml-e2e.log --ttl-sweep-secs 1 & zig-out/bin/multiforadb --port 27020 --db /tmp/mfdb-e2e.log --ttl-sweep-secs 1 &
node tests/e2e/e2e.js # CRUD + operators + aggregate + errors (29 checks) node tests/e2e/e2e.js # CRUD + operators + aggregate + errors (29 checks)
node tests/e2e/e2e2.js concurrent # 8 clients: 4 writers + 4 readers (2 checks) node tests/e2e/e2e2.js concurrent # 8 clients: 4 writers + 4 readers (2 checks)
@@ -43,7 +43,7 @@ E2E6_PORT=27300 node tests/e2e/e2e6.js # different port if 27220 is taken
Rebuild with `zig build` after any change under `src/` before restarting the Rebuild with `zig build` after any change under `src/` before restarting the
server: `zig build test` compiles the test binary only and leaves server: `zig build test` compiles the test binary only and leaves
`zig-out/bin/mongo-lite` stale, so the suites keep running against the old `zig-out/bin/multiforadb` stale, so the suites keep running against the old
rules and report failures that the source no longer explains. rules and report failures that the source no longer explains.
`e2e2.js concurrent` is safe to repeat against a running server (it drops its `e2e2.js concurrent` is safe to repeat against a running server (it drops its
@@ -92,9 +92,9 @@ Measured behavior (all documented in the top-level README):
bash tests/e2e/compare-run.sh [size] [doc-size] # e.g. 1g 16k bash tests/e2e/compare-run.sh [size] [doc-size] # e.g. 1g 16k
``` ```
Starts mongod (`brew install mongodb-community`) on :27018 and mongo-lite Starts mongod (`brew install mongodb-community`) on :27018 and MultiforaDB
on :27019, runs the same driver workload against each (durable writes: on :27019, runs the same driver workload against each (durable writes:
mongo-lite fsyncs per command, mongod runs with `j: true`), measures kill -9 MultiforaDB fsyncs per command, mongod runs with `j: true`), measures kill -9
reopen for both, and prints a side-by-side table. `compare.js` alone runs reopen for both, and prints a side-by-side table. `compare.js` alone runs
one side (see its `--help`-style header comment). one side (see its `--help`-style header comment).
@@ -109,7 +109,7 @@ comparison (`concurrent.js`, N clients each doing sequential `insertOne`
with `{w:1, j:true}` — the group-commit path under real contention), then with `{w:1, j:true}` — the group-commit path under real contention), then
writes a machine-readable, versioned report to writes a machine-readable, versioned report to
`tests/e2e/results/bench-<timestamp>.txt` and prints a diff of the `tests/e2e/results/bench-<timestamp>.txt` and prints a diff of the
mongo-lite numbers against the previous run (`results/bench-latest.txt`). MultiforaDB numbers against the previous run (`results/bench-latest.txt`).
The report has `[main]` / `[concurrency]` / `[meta]` sections with The report has `[main]` / `[concurrency]` / `[meta]` sections with
`name<TAB>value` rows; `bench-run.sh 1g 16k` reproduces the phase8 gate `name<TAB>value` rows; `bench-run.sh 1g 16k` reproduces the phase8 gate
(see `results/phase8.txt`). (see `results/phase8.txt`).

View File

@@ -4,10 +4,10 @@
# bash tests/e2e/bench-run.sh [size] [doc-size] [clients...] # bash tests/e2e/bench-run.sh [size] [doc-size] [clients...]
# (defaults: 1g, 16k, "1 4 8 16 32"; clients apply to the concurrent phase) # (defaults: 1g, 16k, "1 4 8 16 32"; clients apply to the concurrent phase)
# #
# Runs the compare-run.sh main suite (mongo-lite vs mongod, same driver, # Runs the compare-run.sh main suite (multiforadb vs mongod, same driver,
# durable writes) plus the concurrent durable-write comparison, then writes # durable writes) plus the concurrent durable-write comparison, then writes
# a versioned machine-readable report to tests/e2e/results/ and prints a # a versioned machine-readable report to tests/e2e/results/ and prints a
# diff of the mongo-lite numbers against the previous run. # diff of the multiforadb numbers against the previous run.
set -u set -u
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
SIZE="${1:-1g}"; DOC="${2:-16k}"; CLIENTS="${3:-1 4 8 16 32}" SIZE="${1:-1g}"; DOC="${2:-16k}"; CLIENTS="${3:-1 4 8 16 32}"
@@ -35,8 +35,8 @@ echo "### concurrent durable insertOne (clients: $CLIENTS)" >&2
rm -rf "$TMP/mongod" && mkdir -p "$TMP/mongod" rm -rf "$TMP/mongod" && mkdir -p "$TMP/mongod"
mongod --dbpath "$TMP/mongod" --port 27018 --bind_ip 127.0.0.1 --quiet >"$TMP/md.out" 2>&1 & mongod --dbpath "$TMP/mongod" --port 27018 --bind_ip 127.0.0.1 --quiet >"$TMP/md.out" 2>&1 &
MD_PID=$! MD_PID=$!
./zig-out/bin/mongo-lite --port 27019 --db "$TMP/ml.log" --compact-threshold 1g >"$TMP/ml.out" 2>&1 & ./zig-out/bin/multiforadb --port 27019 --db "$TMP/mfdb.log" --compact-threshold 1g >"$TMP/mfdb.out" 2>&1 &
ML_PID=$! MFDB_PID=$!
# Poll both servers with the real driver until they answer (fresh mongod # Poll both servers with the real driver until they answer (fresh mongod
# dbpaths can take several seconds; a fixed sleep is flaky). # dbpaths can take several seconds; a fixed sleep is flaky).
wait_ready() { # $1 = url wait_ready() { # $1 = url
@@ -47,25 +47,25 @@ wait_ready() { # $1 = url
done done
return 1 return 1
} }
wait_ready mongodb://127.0.0.1:27018 || { echo "mongod never became ready" >&2; kill -9 $MD_PID $ML_PID 2>/dev/null; exit 1; } wait_ready mongodb://127.0.0.1:27018 || { echo "mongod never became ready" >&2; kill -9 $MD_PID $MFDB_PID 2>/dev/null; exit 1; }
wait_ready mongodb://127.0.0.1:27019 || { echo "mongo-lite never became ready" >&2; kill -9 $MD_PID $ML_PID 2>/dev/null; exit 1; } wait_ready mongodb://127.0.0.1:27019 || { echo "multiforadb never became ready" >&2; kill -9 $MD_PID $MFDB_PID 2>/dev/null; exit 1; }
CC="$TMP/cc.txt" CC="$TMP/cc.txt"
for C in $CLIENTS; do for C in $CLIENTS; do
MD=$(node tests/e2e/concurrent.js --url mongodb://127.0.0.1:27018 --label mongodb --clients "$C" --per-client "$PER_CLIENT" 2>/dev/null | tail -1) MD=$(node tests/e2e/concurrent.js --url mongodb://127.0.0.1:27018 --label mongodb --clients "$C" --per-client "$PER_CLIENT" 2>/dev/null | tail -1)
ML=$(node tests/e2e/concurrent.js --url mongodb://127.0.0.1:27019 --label mongo-lite --clients "$C" --per-client "$PER_CLIENT" 2>/dev/null | tail -1) MFDB=$(node tests/e2e/concurrent.js --url mongodb://127.0.0.1:27019 --label multiforadb --clients "$C" --per-client "$PER_CLIENT" 2>/dev/null | tail -1)
ML_D=$(echo "$ML" | sed -E 's/.*\t([0-9.]+) docs\/s.*/\1/') MFDB_D=$(echo "$MFDB" | sed -E 's/.*\t([0-9.]+) docs\/s.*/\1/')
MD_D=$(echo "$MD" | sed -E 's/.*\t([0-9.]+) docs\/s.*/\1/') MD_D=$(echo "$MD" | sed -E 's/.*\t([0-9.]+) docs\/s.*/\1/')
if [ -z "$ML_D" ] || [ -z "$MD_D" ]; then if [ -z "$MFDB_D" ] || [ -z "$MD_D" ]; then
echo "WARNING: concurrent run at $C clients produced no result (ml='$ML' md='$MD')" >&2 echo "WARNING: concurrent run at $C clients produced no result (mfdb='$MFDB' md='$MD')" >&2
fi fi
RATIO=$(node -e "const m=Number('$ML_D'),d=Number('$MD_D');console.log(d>0?(m/d).toFixed(1)+'x':'—')") RATIO=$(node -e "const m=Number('$MFDB_D'),d=Number('$MD_D');console.log(d>0?(m/d).toFixed(1)+'x':'—')")
echo "clients $C $ML_D $MD_D $RATIO" | tee -a "$CC" echo "clients $C $MFDB_D $MD_D $RATIO" | tee -a "$CC"
done done
kill -9 $MD_PID $ML_PID 2>/dev/null; wait 2>/dev/null kill -9 $MD_PID $MFDB_PID 2>/dev/null; wait 2>/dev/null
# ---- assemble the report -------------------------------------------------- # ---- assemble the report --------------------------------------------------
{ {
echo "# mongo-lite vs MongoDB benchmark" echo "# multiforadb vs MongoDB benchmark"
echo "# date: $(date -u +%Y-%m-%dT%H:%M:%SZ) git: $REV$DIRTY" echo "# date: $(date -u +%Y-%m-%dT%H:%M:%SZ) git: $REV$DIRTY"
echo "# args: size=$SIZE doc-size=$DOC wc=j clients=$CLIENTS per-client=$PER_CLIENT" echo "# args: size=$SIZE doc-size=$DOC wc=j clients=$CLIENTS per-client=$PER_CLIENT"
echo "# reproduce: bash tests/e2e/bench-run.sh $SIZE $DOC \"$CLIENTS\"" echo "# reproduce: bash tests/e2e/bench-run.sh $SIZE $DOC \"$CLIENTS\""
@@ -81,7 +81,7 @@ kill -9 $MD_PID $ML_PID 2>/dev/null; wait 2>/dev/null
} }
return m; return m;
}; };
const a = read("/tmp/mongo-cmp/ml-report.txt"); const a = read("/tmp/mongo-cmp/mfdb-report.txt");
const b = read("/tmp/mongo-cmp/mongo-report.txt"); const b = read("/tmp/mongo-cmp/mongo-report.txt");
const keys = ["insertOne (sequential) ×200","bulk insert throughput","docs loaded","createIndex({k: 1})", const keys = ["insertOne (sequential) ×200","bulk insert throughput","docs loaded","createIndex({k: 1})",
"countDocuments({})","findOne({_id: <ObjectId>})","findOne({k: 500}) (indexed)","find({p: {$gte,$lt}}).count() (scan)", "countDocuments({})","findOne({_id: <ObjectId>})","findOne({k: 500}) (indexed)","find({p: {$gte,$lt}}).count() (scan)",
@@ -104,7 +104,7 @@ kill -9 $MD_PID $ML_PID 2>/dev/null; wait 2>/dev/null
# ---- diff against the previous run --------------------------------------- # ---- diff against the previous run ---------------------------------------
echo "report: $REPORT" echo "report: $REPORT"
if [ -f "$LATEST" ]; then if [ -f "$LATEST" ]; then
echo; echo "### mongo-lite numbers vs previous run ($(head -2 "$LATEST" | tail -1 | sed 's/# //'))" echo; echo "### multiforadb numbers vs previous run ($(head -2 "$LATEST" | tail -1 | sed 's/# //'))"
node -e ' node -e '
const fs = require("fs"); const fs = require("fs");
const old = fs.readFileSync(process.argv[1], "utf8"); const old = fs.readFileSync(process.argv[1], "utf8");
@@ -136,9 +136,9 @@ if [ -f "$LATEST" ]; then
const d = isFinite(num(ov)) && isFinite(num(v)) && num(ov) > 0 ? ((num(v) - num(ov)) / num(ov) * 100).toFixed(0) + "%" : ""; const d = isFinite(num(ov)) && isFinite(num(v)) && num(ov) > 0 ? ((num(v) - num(ov)) / num(ov) * 100).toFixed(0) + "%" : "";
console.log(k.padEnd(42) + fmt(ov) + fmt(v) + d); console.log(k.padEnd(42) + fmt(ov) + fmt(v) + d);
} }
// concurrency (mongo-lite docs/s per client count) // concurrency (multiforadb docs/s per client count)
const oc = section(old, "concurrency"), nc = section(neu, "concurrency"); const oc = section(old, "concurrency"), nc = section(neu, "concurrency");
console.log("\nconcurrency — mongo-lite docs/s:"); console.log("\nconcurrency — multiforadb docs/s:");
for (const [k, v] of nc) { for (const [k, v] of nc) {
const ov = oc.get(k); const ov = oc.get(k);
const d = ov && Number(ov) > 0 ? ((Number(v) - Number(ov)) / Number(ov) * 100).toFixed(0) + "%" : ""; const d = ov && Number(ov) > 0 ? ((Number(v) - Number(ov)) / Number(ov) * 100).toFixed(0) + "%" : "";

View File

@@ -1,4 +1,4 @@
// Big-collection harness: how mongo-lite behaves with multi-GB collections. // Big-collection harness: how multiforadb behaves with multi-GB collections.
// //
// Spawns its own server, bulk-inserts up to ~5 GB of documents, measures // Spawns its own server, bulk-inserts up to ~5 GB of documents, measures
// insert throughput, log/compaction behavior and server RSS, benchmarks // insert throughput, log/compaction behavior and server RSS, benchmarks
@@ -22,14 +22,14 @@
// --keep keep the db file after the run // --keep keep the db file after the run
// --quick tiny run (256m, 16k docs) // --quick tiny run (256m, 16k docs)
// //
// Env: ML_BIN server binary (default ../../zig-out/bin/mongo-lite) // Env: MFDB_BIN server binary (default ../../zig-out/bin/multiforadb)
// BIG_DB db file path (default .zig-cache/big.log) // BIG_DB db file path (default .zig-cache/big.log)
const { MongoClient } = require('mongodb'); const { MongoClient } = require('mongodb');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const BIN = process.env.ML_BIN || path.resolve(__dirname, '../../zig-out/bin/mongo-lite'); const BIN = process.env.MFDB_BIN || path.resolve(__dirname, '../../zig-out/bin/multiforadb');
const PORT = Number(process.env.BIG_PORT || 27221); const PORT = Number(process.env.BIG_PORT || 27221);
const DBFILE = process.env.BIG_DB || path.resolve(__dirname, '../../.zig-cache/big.log'); const DBFILE = process.env.BIG_DB || path.resolve(__dirname, '../../.zig-cache/big.log');
const URL = `mongodb://127.0.0.1:${PORT}`; const URL = `mongodb://127.0.0.1:${PORT}`;
@@ -137,7 +137,7 @@ async function main() {
} }
fs.rmSync(DBFILE, { force: true }); fs.rmSync(DBFILE, { force: true });
const fmt = (n) => (n >= 1e9 ? (n / 1e9).toFixed(2) + ' GB' : n >= 1e6 ? (n / 1e6).toFixed(1) + ' MB' : n >= 1e3 ? (n / 1e3).toFixed(1) + ' KB' : n + ' B'); const fmt = (n) => (n >= 1e9 ? (n / 1e9).toFixed(2) + ' GB' : n >= 1e6 ? (n / 1e6).toFixed(1) + ' MB' : n >= 1e3 ? (n / 1e3).toFixed(1) + ' KB' : n + ' B');
console.log(`mongo-lite big-collection harness`); console.log(`multiforadb big-collection harness`);
console.log(` size ${fmt(SIZE)} · doc ~${fmt(DOC_SIZE)} · batch ${opt.batch} · index ${opt.index || 'none'} · ids ${opt.oid ? 'ObjectId' : 'int'} · compact-threshold ${opt.compactThreshold || '16m'} · db ${DBFILE}`); console.log(` size ${fmt(SIZE)} · doc ~${fmt(DOC_SIZE)} · batch ${opt.batch} · index ${opt.index || 'none'} · ids ${opt.oid ? 'ObjectId' : 'int'} · compact-threshold ${opt.compactThreshold || '16m'} · db ${DBFILE}`);
console.log('\n== server start (fresh log) =='); console.log('\n== server start (fresh log) ==');
@@ -331,7 +331,7 @@ async function main() {
await stopServer('SIGKILL'); await stopServer('SIGKILL');
console.log('\n== summary =='); console.log('\n== summary ==');
console.log(` mongo-lite handles a ${fmt(bytes)} collection fully in RAM (RSS ${peakRss} MB)`); console.log(` multiforadb handles a ${fmt(bytes)} collection fully in RAM (RSS ${peakRss} MB)`);
console.log(` insert: ${(bytes / 1e6 / (insertMs / 1000)).toFixed(1)} MB/s — fsync per write is by design (crash safety)`); console.log(` insert: ${(bytes / 1e6 / (insertMs / 1000)).toFixed(1)} MB/s — fsync per write is by design (crash safety)`);
if (compactions > 0) { if (compactions > 0) {
console.log(` ${compactions} compaction rewrites observed: every 16MB of writes rewrites the whole log — for multi-GB loads the cumulative rewrite traffic dominates`); console.log(` ${compactions} compaction rewrites observed: every 16MB of writes rewrites the whole log — for multi-GB loads the cumulative rewrite traffic dominates`);

View File

@@ -1,26 +1,26 @@
#!/bin/bash #!/bin/bash
# Compare mongo-lite against a real MongoDB with the same workload, same driver. # Compare multiforadb against a real MongoDB with the same workload, same driver.
# #
# bash tests/e2e/compare-run.sh [size] [doc-size] # bash tests/e2e/compare-run.sh [size] [doc-size]
# (defaults: 1g, 16k) # (defaults: 1g, 16k)
# #
# Starts mongod on :27018 and mongo-lite on :27019, runs compare.js against # Starts mongod on :27018 and multiforadb on :27019, runs compare.js against
# each (durable writes: mongo-lite fsyncs per doc, mongod ack'd with j:true), # each (durable writes: multiforadb fsyncs per doc, mongod ack'd with j:true),
# measures kill -9 reopen time for both, and prints a side-by-side table. # measures kill -9 reopen time for both, and prints a side-by-side table.
set -u set -u
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
SIZE="${1:-1g}" SIZE="${1:-1g}"
DOC="${2:-16k}" DOC="${2:-16k}"
echo "comparing mongo-lite vs mongodb — dataset ${SIZE}, docs ~${DOC}" echo "comparing multiforadb vs mongodb — dataset ${SIZE}, docs ~${DOC}"
CMPDIR=/tmp/mongo-cmp CMPDIR=/tmp/mongo-cmp
mkdir -p "$CMPDIR/mongod" mkdir -p "$CMPDIR/mongod"
ML_LOG="$CMPDIR/ml.log" MFDB_LOG="$CMPDIR/mfdb.log"
ML_OUT="$CMPDIR/ml-srv.out" MFDB_OUT="$CMPDIR/mfdb-srv.out"
MD_OUT="$CMPDIR/md-srv.out" MD_OUT="$CMPDIR/md-srv.out"
ML_PORT=27019 MFDB_PORT=27019
MD_PORT=27018 MD_PORT=27018
rm -f "$ML_LOG" rm -f "$MFDB_LOG"
rm -rf "$CMPDIR/mongod" && mkdir -p "$CMPDIR/mongod" rm -rf "$CMPDIR/mongod" && mkdir -p "$CMPDIR/mongod"
# ---- MongoDB ------------------------------------------------------------- # ---- MongoDB -------------------------------------------------------------
@@ -60,26 +60,26 @@ setTimeout(poll, 300);
') ')
kill -9 $MD_PID 2>/dev/null kill -9 $MD_PID 2>/dev/null
# ---- mongo-lite ---------------------------------------------------------- # ---- multiforadb ----------------------------------------------------------
echo; echo "### mongo-lite (recommended config: --compact-threshold 1g)" echo; echo "### multiforadb (recommended config: --compact-threshold 1g)"
# Debug is ~10-200x slower (see the README's perf section) — the comparison # Debug is ~10-200x slower (see the README's perf section) — the comparison
# must use the optimized build. # must use the optimized build.
zig build -Doptimize=ReleaseFast 2>&1 | grep -c "^error" | grep -q "^0" || { echo "build failed"; exit 1; } zig build -Doptimize=ReleaseFast 2>&1 | grep -c "^error" | grep -q "^0" || { echo "build failed"; exit 1; }
./zig-out/bin/mongo-lite --port $ML_PORT --db "$ML_LOG" --compact-threshold 1g >"$ML_OUT" 2>&1 & ./zig-out/bin/multiforadb --port $MFDB_PORT --db "$MFDB_LOG" --compact-threshold 1g >"$MFDB_OUT" 2>&1 &
ML_PID=$! MFDB_PID=$!
wait_ready "mongodb://127.0.0.1:$ML_PORT" || { echo "mongo-lite never became ready" >&2; exit 1; } wait_ready "mongodb://127.0.0.1:$MFDB_PORT" || { echo "multiforadb never became ready" >&2; exit 1; }
node tests/e2e/compare.js --url "mongodb://127.0.0.1:$ML_PORT" --label mongo-lite --size "$SIZE" --doc-size "$DOC" \ node tests/e2e/compare.js --url "mongodb://127.0.0.1:$MFDB_PORT" --label multiforadb --size "$SIZE" --doc-size "$DOC" \
> "$CMPDIR/ml-report.txt" 2>&1 || { echo "mongo-lite bench failed:"; tail -5 "$CMPDIR/ml-report.txt"; } > "$CMPDIR/mfdb-report.txt" 2>&1 || { echo "multiforadb bench failed:"; tail -5 "$CMPDIR/mfdb-report.txt"; }
ML_RSS=$(ps -o rss= -p $ML_PID | awk '{printf "%.0f", $1/1024}') MFDB_RSS=$(ps -o rss= -p $MFDB_PID | awk '{printf "%.0f", $1/1024}')
ML_DISK=$(du -sm "$ML_LOG" | awk '{print $1}') MFDB_DISK=$(du -sm "$MFDB_LOG" | awk '{print $1}')
echo; echo "### mongo-lite kill -9 + reopen (replay)" echo; echo "### multiforadb kill -9 + reopen (replay)"
kill -9 $ML_PID; wait $ML_PID 2>/dev/null kill -9 $MFDB_PID; wait $MFDB_PID 2>/dev/null
ML_REOPEN=$(cd tests/e2e && node -e ' MFDB_REOPEN=$(cd tests/e2e && node -e '
const { spawn } = require("child_process"); const { spawn } = require("child_process");
const { MongoClient } = require("mongodb"); const { MongoClient } = require("mongodb");
const t0 = Date.now(); const t0 = Date.now();
const p = spawn("/Users/shkmv/workspace/sandbox/mongo-lite/zig-out/bin/mongo-lite", ["--port","27019","--db","/tmp/mongo-cmp/ml.log","--compact-threshold","1g"], {stdio:"ignore"}); const p = spawn("../../zig-out/bin/multiforadb", ["--port","27019","--db","/tmp/mongo-cmp/mfdb.log","--compact-threshold","1g"], {stdio:"ignore"});
const poll = async () => { const poll = async () => {
const c = new MongoClient("mongodb://127.0.0.1:27019", {serverSelectionTimeoutMS: 800}); const c = new MongoClient("mongodb://127.0.0.1:27019", {serverSelectionTimeoutMS: 800});
try { await c.connect(); await c.db("admin").command({ping:1}); await c.close(); p.kill("SIGKILL"); console.log(((Date.now()-t0)/1000).toFixed(1)); } try { await c.connect(); await c.db("admin").command({ping:1}); await c.close(); p.kill("SIGKILL"); console.log(((Date.now()-t0)/1000).toFixed(1)); }
@@ -87,12 +87,12 @@ const poll = async () => {
}; };
setTimeout(poll, 300); setTimeout(poll, 300);
') ')
kill -9 $ML_PID 2>/dev/null kill -9 $MFDB_PID 2>/dev/null
# ---- side by side ---------------------------------------------------------- # ---- side by side ----------------------------------------------------------
echo; echo "### side by side — ${SIZE} dataset, ~${DOC} docs" echo; echo "### side by side — ${SIZE} dataset, ~${DOC} docs"
cat > "$CMPDIR/meta.json" <<EOF cat > "$CMPDIR/meta.json" <<EOF
{"ml_rss_mb": "$ML_RSS", "md_rss_mb": "$MD_RSS", "ml_reopen": "${ML_REOPEN}s", "md_reopen": "${MD_REOPEN}s", "ml_disk_mb": "${ML_DISK}MB", "md_disk_mb": "${MD_DISK}MB"} {"mfdb_rss_mb": "$MFDB_RSS", "md_rss_mb": "$MD_RSS", "mfdb_reopen": "${MFDB_REOPEN}s", "md_reopen": "${MD_REOPEN}s", "mfdb_disk_mb": "${MFDB_DISK}MB", "md_disk_mb": "${MD_DISK}MB"}
EOF EOF
node -e ' node -e '
const fs = require("fs"); const fs = require("fs");
@@ -105,7 +105,7 @@ const read = (p) => {
} }
return m; return m;
}; };
const a = read("/tmp/mongo-cmp/ml-report.txt"); const a = read("/tmp/mongo-cmp/mfdb-report.txt");
const b = read("/tmp/mongo-cmp/mongo-report.txt"); const b = read("/tmp/mongo-cmp/mongo-report.txt");
const meta = JSON.parse(fs.readFileSync("/tmp/mongo-cmp/meta.json", "utf8")); const meta = JSON.parse(fs.readFileSync("/tmp/mongo-cmp/meta.json", "utf8"));
const keys = ["insertOne (sequential) ×200","bulk insert throughput","docs loaded","createIndex({k: 1})", const keys = ["insertOne (sequential) ×200","bulk insert throughput","docs loaded","createIndex({k: 1})",
@@ -113,17 +113,17 @@ const keys = ["insertOne (sequential) ×200","bulk insert throughput","docs load
"find({}).sort({_id:-1}).limit(20)","find({}, {proj}).limit(1000)","aggregate $group by k", "find({}).sort({_id:-1}).limit(20)","find({}, {proj}).limit(1000)","aggregate $group by k",
"updateOne({_id}) ×50","updateMany({k: 7}, {$inc})","deleteOne({_id}) + insertOne","node client RSS"]; "updateOne({_id}) ×50","updateMany({k: 7}, {$inc})","deleteOne({_id}) + insertOne","node client RSS"];
const col = (v) => String(v).padEnd(22); const col = (v) => String(v).padEnd(22);
console.log(`${"benchmark".padEnd(42)} ${col("mongo-lite")} ${col("mongodb")} ratio`); console.log(`${"benchmark".padEnd(42)} ${col("multiforadb")} ${col("mongodb")} ratio`);
for (const k of keys) { for (const k of keys) {
const av = a[k] || "—", bv = b[k] || "—"; const av = a[k] || "—", bv = b[k] || "—";
const ar = parseFloat(av), br = parseFloat(bv); const ar = parseFloat(av), br = parseFloat(bv);
const ratio = isFinite(ar) && isFinite(br) && ar > 0 && br > 0 ? (ar / br).toFixed(1) + "x" : ""; const ratio = isFinite(ar) && isFinite(br) && ar > 0 && br > 0 ? (ar / br).toFixed(1) + "x" : "";
console.log(`${k.padEnd(42)} ${col(av)} ${col(bv)} ${ratio}`); console.log(`${k.padEnd(42)} ${col(av)} ${col(bv)} ${ratio}`);
} }
console.log(`${`server RSS`.padEnd(42)} ${col(meta.ml_rss_mb + " MB")} ${col(meta.md_rss_mb + " MB")}`); console.log(`${`server RSS`.padEnd(42)} ${col(meta.mfdb_rss_mb + " MB")} ${col(meta.md_rss_mb + " MB")}`);
console.log(`${`kill -9 reopen`.padEnd(42)} ${col(meta.ml_reopen)} ${col(meta.md_reopen)}`); console.log(`${`kill -9 reopen`.padEnd(42)} ${col(meta.mfdb_reopen)} ${col(meta.md_reopen)}`);
console.log(`${`db on disk`.padEnd(42)} ${col(meta.ml_disk_mb)} ${col(meta.md_disk_mb)}`); console.log(`${`db on disk`.padEnd(42)} ${col(meta.mfdb_disk_mb)} ${col(meta.md_disk_mb)}`);
' '
pkill -9 -f "mongo-lite --port $ML_PORT" 2>/dev/null pkill -9 -f "multiforadb --port $MFDB_PORT" 2>/dev/null
echo; echo "done — reports: $CMPDIR/ml-report.txt, $CMPDIR/mongo-report.txt" echo; echo "done — reports: $CMPDIR/mfdb-report.txt, $CMPDIR/mongo-report.txt"

View File

@@ -1,8 +1,8 @@
// Benchmark: the same workload through the official driver against mongo-lite // Benchmark: the same workload through the official driver against multiforadb
// and a real MongoDB. Run once per server URL, then diff the reports. // and a real MongoDB. Run once per server URL, then diff the reports.
// //
// node tests/e2e/compare.js --url mongodb://127.0.0.1:27018 --label mongodb // node tests/e2e/compare.js --url mongodb://127.0.0.1:27018 --label mongodb
// node tests/e2e/compare.js --url mongodb://127.0.0.1:27019 --label mongo-lite // node tests/e2e/compare.js --url mongodb://127.0.0.1:27019 --label multiforadb
// //
// Options: // Options:
// --url <u> server URL (required) // --url <u> server URL (required)
@@ -12,11 +12,11 @@
// --batch <n> docs per insertMany (default 500) // --batch <n> docs per insertMany (default 500)
// --index <field> field to index before the op benchmarks (default k) // --index <field> field to index before the op benchmarks (default k)
// --wc <j|none> write concern: 'j' = {w:1, j:true} durable ack on every // --wc <j|none> write concern: 'j' = {w:1, j:true} durable ack on every
// write (fair vs mongo-lite's fsync-per-write); 'none' = // write (fair vs multiforadb's fsync-per-write); 'none' =
// driver default (default j) // driver default (default j)
// //
// Every benchmark is awaited (no fire-and-forget), which is exactly how the // Every benchmark is awaited (no fire-and-forget), which is exactly how the
// big.js harness measures mongo-lite, so the numbers are directly comparable. // big.js harness measures multiforadb, so the numbers are directly comparable.
const { MongoClient, ObjectId } = require('mongodb'); const { MongoClient, ObjectId } = require('mongodb');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
@@ -118,7 +118,7 @@ async function main() {
try { collInfo = await db.command({ collStats: 'items' }); } catch {} try { collInfo = await db.command({ collStats: 'items' }); } catch {}
if (collInfo) row('server-side data size', fmt(collInfo.size ?? 0), `storage ${fmt(collInfo.storageSize ?? 0)}`); if (collInfo) row('server-side data size', fmt(collInfo.size ?? 0), `storage ${fmt(collInfo.storageSize ?? 0)}`);
// ---- secondary index (mongo-lite: planner uses it; mongodb: normal) ------ // ---- secondary index (multiforadb: planner uses it; mongodb: normal) ------
if (opt.index) { if (opt.index) {
await bench(`createIndex({${opt.index}: 1})`, async () => { await coll.createIndex({ [opt.index]: 1 }); }); await bench(`createIndex({${opt.index}: 1})`, async () => { await coll.createIndex({ [opt.index]: 1 }); });
} }

View File

@@ -2,7 +2,7 @@
// insertOne ({w:1, j:true} by default) into their own collection, reporting // insertOne ({w:1, j:true} by default) into their own collection, reporting
// aggregate docs/s. Exercises the group-commit path under real contention. // aggregate docs/s. Exercises the group-commit path under real contention.
// //
// node tests/e2e/concurrent.js --url mongodb://127.0.0.1:27019 --label mongo-lite // node tests/e2e/concurrent.js --url mongodb://127.0.0.1:27019 --label multiforadb
// [--clients 8] [--per-client 2000] [--wc j|none] // [--clients 8] [--per-client 2000] [--wc j|none]
// //
// Output (stdout, tab-separated, one line): // Output (stdout, tab-separated, one line):

View File

@@ -1,4 +1,4 @@
// End-to-end test: official MongoDB Node.js driver against mongo-lite. // End-to-end test: official MongoDB Node.js driver against multiforadb.
const { MongoClient, ObjectId } = require('mongodb'); const { MongoClient, ObjectId } = require('mongodb');
const URL = 'mongodb://127.0.0.1:27020'; const URL = 'mongodb://127.0.0.1:27020';

View File

@@ -4,7 +4,7 @@
// server must reject with CannotCreateIndex (67). // server must reject with CannotCreateIndex (67).
// //
// The server must run with a short sweep interval, e.g. // The server must run with a short sweep interval, e.g.
// zig-out/bin/mongo-lite --port 27020 --db /tmp/ml-e2e.log --ttl-sweep-secs 1 // zig-out/bin/multiforadb --port 27020 --db /tmp/mfdb-e2e.log --ttl-sweep-secs 1
const { MongoClient } = require('mongodb'); const { MongoClient } = require('mongodb');
const URL = 'mongodb://127.0.0.1:27020'; const URL = 'mongodb://127.0.0.1:27020';

View File

@@ -1,6 +1,6 @@
// E2E part 6: the full lifecycle, self-contained. // E2E part 6: the full lifecycle, self-contained.
// //
// Spawns its own mongo-lite server on a fresh log file and drives the whole // Spawns its own multiforadb server on a fresh log file and drives the whole
// feature surface through the official driver: CRUD + query operators + // feature surface through the official driver: CRUD + query operators +
// aggregation + error codes + secondary indexes + TTL expiry + admin // aggregation + error codes + secondary indexes + TTL expiry + admin
// commands, then restarts the server twice — once gracefully, once with // commands, then restarts the server twice — once gracefully, once with
@@ -12,7 +12,7 @@
// node tests/e2e/e2e6.js // node tests/e2e/e2e6.js
// //
// Env: E2E6_PORT listen port (default 27220) // Env: E2E6_PORT listen port (default 27220)
// ML_BIN server binary (default ../../zig-out/bin/mongo-lite) // MFDB_BIN server binary (default ../../zig-out/bin/multiforadb)
// E2E6_KEEP keep the log file after the run // E2E6_KEEP keep the log file after the run
const { MongoClient, ObjectId } = require('mongodb'); const { MongoClient, ObjectId } = require('mongodb');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
@@ -20,7 +20,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const PORT = Number(process.env.E2E6_PORT || 27220); const PORT = Number(process.env.E2E6_PORT || 27220);
const BIN = process.env.ML_BIN || path.resolve(__dirname, '../../zig-out/bin/mongo-lite'); const BIN = process.env.MFDB_BIN || path.resolve(__dirname, '../../zig-out/bin/multiforadb');
const DBFILE = process.env.E2E6_DB || path.resolve(__dirname, '../../.zig-cache/e2e6-full.log'); const DBFILE = process.env.E2E6_DB || path.resolve(__dirname, '../../.zig-cache/e2e6-full.log');
const URL = `mongodb://127.0.0.1:${PORT}`; const URL = `mongodb://127.0.0.1:${PORT}`;

View File

@@ -1,7 +1,7 @@
{ {
"name": "e2e", "name": "e2e",
"version": "1.0.0", "version": "1.0.0",
"description": "These exercise mongo-lite from a real driver over TCP: full CRUD, query operators, aggregation, error codes, concurrent clients, and crash recovery.", "description": "These exercise multiforadb from a real driver over TCP: full CRUD, query operators, aggregation, error codes, concurrent clients, and crash recovery.",
"main": "e2e.js", "main": "e2e.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"