wire/server: honour moreToCome on OP_MSG requests

`Message.flags` was parsed and stored but never read. An OP_MSG request with
moreToCome set is fire-and-forget: the client will not read a reply. Sending one
anyway leaves it unread in the socket, so the next command on that connection
reads the previous command's reply and waits forever for its own.

This is not a corner case. Every unacknowledged write uses it, and the Node
driver sends `endSessions` with `writeConcern: {w: 0}` whenever a client closes
-- so an ordinary application that never asks for w:0 still hits it. Before:

  insertOne({w: 0})            -> ok, acknowledged=false
  countDocuments() (same conn) -> BSON element "cursor" is missing

The command still runs; only the reply is suppressed.

The e2e case pins maxPoolSize to 1, because with a larger pool the driver may
hand the next operation a different connection and hide the bug. It asserts the
connection still works afterwards, which is the part that matters -- not that
the unacknowledged write itself returned.
This commit is contained in:
2026-08-03 18:55:50 +03:00
parent d867c37d32
commit e2c25a986b
3 changed files with 72 additions and 0 deletions

View File

@@ -170,6 +170,24 @@ fn handle_connection_inner(io: std.Io, stream: std.Io.net.Stream, server: *Serve
) 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)

View File

@@ -28,6 +28,18 @@ pub const Message = struct {
body: bson.Document,
seqs: []const Seq,
/// OP_MSG flagBits: bit 0 checksumPresent, bit 1 moreToCome, bit 16
/// exhaustAllowed.
pub const flag_more_to_come: u32 = 1 << 1;
/// Whether the client declared it will not read a reply to this request
/// (unacknowledged writes, `endSessions`). Answering one anyway leaves an
/// unread reply in the socket and desyncs every later command on the
/// connection -- see the handler in server.zig.
pub fn more_to_come(self: *const Message) bool {
return self.flags & flag_more_to_come != 0;
}
pub fn parse(gpa: std.mem.Allocator, bytes: []const u8) !Message {
if (bytes.len < 16) return error.InvalidMessage;
const total: u32 = std.mem.readInt(u32, bytes[0..4], .little);