The point of a lagging checkpoint: a record whose effect the data file already holds is redundant, so the log can go back to just its header. Without this the log only grows and every open pays for every write ever made. Ordering, which is the whole safety argument: publish the watermark, *then* truncate. The other way round, a crash between them leaves the records gone from the log and absent from any image. A failed truncation is a warning rather than an error -- it costs space and replay time, and loses nothing, so it must not fail a checkpoint that already succeeded. Also wires checkpointing up, which nothing did before. `note_checkpoint` arms it when the log passes a threshold, and the write epilogue and the TTL monitor both claim it -- outside any collection lock, for the same reason compaction runs there: it takes the log lock. The threshold is separate from the compaction one on purpose: compaction is about the garbage share of the data, a checkpoint is about how much replay an open would otherwise do. -- Two things the tests taught me. The first version measured the log before the checkpoint and found 16 bytes -- just the header. Appends buffer in the log's open block and only a commit seals and writes it, so there was nothing on disk to shrink. The test commits first now, and says why. And the "no valid watermark" warning fired for every young database, which is its normal state before the first checkpoint. It now distinguishes a watermark that was *written and cannot be read* from one that was never written -- warning about the ordinary case is how people learn to ignore the warning that matters. Mutation-checked, red: skipping the truncation. Not covered, and the test says so: moving the truncation before the publish, whose failure mode is a crash landing between the two. That needs process-level crash injection, which an in-process test cannot express.
226 lines
9.3 KiB
Zig
226 lines
9.3 KiB
Zig
//! TCP server speaking the MongoDB wire protocol. Accept loop dispatches each
|
|
//! connection onto the Io worker pool; a connection is handled until the peer
|
|
//! closes it or a protocol error occurs.
|
|
|
|
const std = @import("std");
|
|
const bson = @import("bson.zig");
|
|
const wire = @import("wire.zig");
|
|
const commands = @import("commands.zig");
|
|
const db = @import("db.zig");
|
|
|
|
pub const Server = struct {
|
|
gpa: std.mem.Allocator,
|
|
port: u16,
|
|
bind_ip: []const u8,
|
|
oid_gen: bson.ObjectIdGen,
|
|
connection_counter: std.atomic.Value(u32),
|
|
engine: *db.Engine,
|
|
start_time: std.Io.Timestamp,
|
|
/// Seconds between TTL sweeps; 0 leaves the monitor unspawned. Signed
|
|
/// because that is what std.Io.Duration.fromSeconds takes — the CLI
|
|
/// rejects negatives.
|
|
ttl_sweep_secs: i64,
|
|
|
|
pub fn run(self: *Server) !void {
|
|
// Unbounded async limit: connection handlers otherwise fall back to
|
|
// running inline on the accept-loop fiber once busy_count hits the
|
|
// default cpu_count-1, which blocks accept() for the handler's
|
|
// lifetime and stalls new connections (handshake timeouts). With an
|
|
// unlimited limit the pool spawns a thread per live connection.
|
|
var threaded: std.Io.Threaded = std.Io.Threaded.init(self.gpa, .{ .async_limit = .unlimited });
|
|
defer threaded.deinit();
|
|
const io = threaded.io();
|
|
|
|
var addr = try std.Io.net.IpAddress.parse(self.bind_ip, self.port);
|
|
var listener = try addr.listen(io, .{ .reuse_address = true });
|
|
defer listener.deinit(io);
|
|
|
|
std.debug.print("multiforadb: listening on {s}:{d}\n", .{ self.bind_ip, self.port });
|
|
|
|
var group: std.Io.Group = .init;
|
|
defer group.cancel(io);
|
|
|
|
// The TTL monitor is just another member of the connection group, so
|
|
// the `group.cancel` above stops it with everything else.
|
|
if (self.ttl_sweep_secs > 0) group.async(io, ttl_monitor, .{ io, self });
|
|
|
|
while (true) {
|
|
const stream = listener.accept(io) catch |err| switch (err) {
|
|
error.Canceled => return,
|
|
else => {
|
|
std.debug.print("multiforadb: accept error: {s}\n", .{@errorName(err)});
|
|
continue;
|
|
},
|
|
};
|
|
group.async(io, handle_connection, .{ io, stream, self });
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Expire documents under TTL indexes every `ttl_sweep_secs` seconds, until
|
|
/// the group is canceled. The sweep takes the catalog lock and one
|
|
/// collection's write lock at a time, exactly like a write command; a sweep
|
|
/// failure is logged rather than fatal, since the next one will retry.
|
|
fn ttl_monitor(io: std.Io, server: *Server) error{Canceled}!void {
|
|
const interval: std.Io.Duration = .fromSeconds(server.ttl_sweep_secs);
|
|
while (true) {
|
|
// Sleep first: at startup the engine has just replayed the log, and
|
|
// an immediate sweep would race the listener's first connections for
|
|
// the collection locks.
|
|
try std.Io.sleep(io, interval, .awake);
|
|
const now_ms = std.Io.Timestamp.now(io, .real).toMilliseconds();
|
|
_ = server.engine.ttl_sweep(now_ms) catch |err| {
|
|
std.debug.print("multiforadb: TTL sweep failed: {s}\n", .{@errorName(err)});
|
|
continue;
|
|
};
|
|
if (server.engine.take_checkpoint()) {
|
|
server.engine.checkpoint() catch |err| {
|
|
std.debug.print("multiforadb: checkpoint failed: {s}\n", .{@errorName(err)});
|
|
server.engine.checkpoint_pending.store(true, .release);
|
|
};
|
|
}
|
|
if (server.engine.take_compact()) {
|
|
server.engine.compact() catch |err| {
|
|
std.debug.print("multiforadb: compaction failed: {s}\n", .{@errorName(err)});
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Entry point required by `Group.async`: must return only `error.Canceled`.
|
|
fn handle_connection(io: std.Io, stream: std.Io.net.Stream, server: *Server) error{Canceled}!void {
|
|
handle_connection_inner(io, stream, server) catch {};
|
|
}
|
|
|
|
fn handle_connection_inner(io: std.Io, stream: std.Io.net.Stream, server: *Server) !void {
|
|
defer stream.close(io);
|
|
|
|
const connection_id = server.connection_counter.fetchAdd(1, .monotonic);
|
|
var read_scratch: [16 * 1024]u8 = undefined;
|
|
var write_scratch: [16 * 1024]u8 = undefined;
|
|
var reader = stream.reader(io, &read_scratch);
|
|
var writer = stream.writer(io, &write_scratch);
|
|
|
|
var msg_buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer msg_buf.deinit(server.gpa);
|
|
var out_buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer out_buf.deinit(server.gpa);
|
|
|
|
var reply_request_id: u32 = 1;
|
|
// One reply for the whole connection, reset per request: its arena
|
|
// keeps its pages instead of being rebuilt for every command.
|
|
var reply = wire.Reply.init(server.gpa);
|
|
defer reply.deinit();
|
|
var ctx = commands.Context{
|
|
.gpa = server.gpa,
|
|
.io = io,
|
|
.oid_gen = &server.oid_gen,
|
|
.connection_id = connection_id,
|
|
.client_desc = "127.0.0.1:0",
|
|
.engine = server.engine,
|
|
.server_start = server.start_time,
|
|
};
|
|
|
|
while (true) {
|
|
var len_bytes: [4]u8 = undefined;
|
|
reader.interface.readSliceAll(&len_bytes) catch return; // clean client disconnect (EOF or RST)
|
|
const total: u32 = std.mem.readInt(u32, &len_bytes, .little);
|
|
if (total < 16 or total > wire.max_message_size) {
|
|
std.debug.print("multiforadb: bad message length {d} on conn {d}\n", .{
|
|
total,
|
|
connection_id,
|
|
});
|
|
return;
|
|
}
|
|
|
|
msg_buf.clearRetainingCapacity();
|
|
try msg_buf.ensureTotalCapacity(server.gpa, total);
|
|
msg_buf.items.len = total;
|
|
std.mem.writeInt(u32, msg_buf.items[0..4], total, .little);
|
|
reader.interface.readSliceAll(msg_buf.items[4..]) catch |err| {
|
|
std.debug.print("multiforadb: read error on conn {d}: {s} (body, len {d})\n", .{
|
|
connection_id,
|
|
@errorName(err),
|
|
total,
|
|
});
|
|
return;
|
|
};
|
|
|
|
var msg = wire.Message.parse(server.gpa, msg_buf.items) catch |err| {
|
|
// 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;
|
|
std.debug.print("multiforadb: bad message on conn {d}: {s} (opCode {d})\n", .{
|
|
connection_id,
|
|
@errorName(err),
|
|
op,
|
|
});
|
|
return;
|
|
};
|
|
defer msg.deinit();
|
|
|
|
reply.reset();
|
|
|
|
commands.dispatch(&ctx, &msg, &reply) catch |err| {
|
|
// Discard any partial reply (the client would read the first
|
|
// ok field, which may already say 1) and send a clean error.
|
|
std.debug.print("multiforadb: dispatch error on conn {d} cmd {s}: {s}\n", .{
|
|
connection_id,
|
|
msg.command_name(),
|
|
@errorName(err),
|
|
});
|
|
reply.pairs.clearRetainingCapacity();
|
|
reply.put_error(
|
|
@intFromEnum(commands.ErrorCode.internal_error),
|
|
"InternalError",
|
|
"internal error",
|
|
) catch return;
|
|
};
|
|
|
|
// An OP_MSG request with moreToCome set is fire-and-forget: the driver
|
|
// will not read a reply, so sending one leaves it unread in the socket
|
|
// and every later command on this connection reads the wrong one. The
|
|
// command still runs -- only the reply is suppressed.
|
|
//
|
|
// This is not an obscure corner. The Node driver sends `endSessions`
|
|
// with `writeConcern: {w: 0}` when a client closes, and every
|
|
// unacknowledged write uses the same mechanism. Ignoring the flag
|
|
// desynced the connection on first use, which surfaced as an
|
|
// unrelated-looking timeout on the *next* command:
|
|
//
|
|
// insert crud-v1.coll ... Command failed, durationMS: 8004,
|
|
// failure: 'connection 2 to 127.0.0.1:27222 timed out'
|
|
//
|
|
// The engine was answering in microseconds the whole time; the driver
|
|
// was waiting for a reply it had already been sent, out of order.
|
|
if (msg.op_code == wire.op_code_msg and msg.more_to_come()) continue;
|
|
|
|
out_buf.clearRetainingCapacity();
|
|
const built = if (msg.op_code == wire.op_code_query)
|
|
wire.write_reply_query(server.gpa, reply_request_id, msg.request_id, reply.pairs.items, &out_buf)
|
|
else
|
|
reply.build(server.gpa, reply_request_id, msg.request_id, &out_buf);
|
|
built catch |err| {
|
|
std.debug.print("multiforadb: reply build error on conn {d}: {s}\n", .{
|
|
connection_id,
|
|
@errorName(err),
|
|
});
|
|
return;
|
|
};
|
|
reply_request_id +%= 1;
|
|
writer.interface.writeAll(out_buf.items) catch |err| {
|
|
std.debug.print("multiforadb: write error on conn {d}: {s}\n", .{
|
|
connection_id,
|
|
@errorName(err),
|
|
});
|
|
return;
|
|
};
|
|
writer.interface.flush() catch |err| {
|
|
std.debug.print("multiforadb: flush error on conn {d}: {s}\n", .{
|
|
connection_id,
|
|
@errorName(err),
|
|
});
|
|
return;
|
|
};
|
|
}
|
|
}
|