commands/e2e: drop topologyVersion from the handshake; rename to mongo-lite
Advertising topologyVersion in the hello reply is what tells a driver the server speaks the streaming (awaitable) hello protocol — in the Node driver it is the only condition checked. From the second heartbeat on, the driver then monitored with an exhaust hello (exhaustAllowed + maxAwaitTimeMS) and waited for a stream of replies carrying moreToCome. We answered once with the flag clear and went back to reading, so every heartbeat failed with "Server ended moreToCome unexpectedly", destroying the connection and clearing the pool. MongoDB Compass showed this as a connect/disconnect loop once per heartbeat. We do not implement streaming hello, so we must not claim to. Omitting the field keeps monitoring on the polling path, and agrees with the maxWireVersion 8 we report: streaming hello arrived in wire version 9. The existing e2e files all passed against the broken server — they issue their commands and exit before the second heartbeat — so e2e5 watches SDAM heartbeats on an idle connection instead. Also renames mongo-light to mongo-lite throughout (binary, log messages, docs, gitVersion). Unrelated to the fix above, but squashed in at request rather than left as a commit whose message described only the fix.
This commit is contained in:
@@ -131,11 +131,17 @@ fn add_server_info(ctx: *Context, reply: *wire.Reply) !void {
|
||||
try reply.put("maxWireVersion", .{ .int32 = 8 });
|
||||
try reply.put("readOnly", .{ .bool = false });
|
||||
|
||||
const oid = ctx.oid_gen.new(ctx.io);
|
||||
const tv = try reply.arena_alloc().alloc(bson.Pair, 2);
|
||||
tv[0] = .{ .key = "processId", .value = .{ .object_id = oid } };
|
||||
tv[1] = .{ .key = "counter", .value = .{ .int64 = 0 } };
|
||||
try reply.put("topologyVersion", .{ .doc = tv });
|
||||
// Deliberately no `topologyVersion`. A driver treats its presence as
|
||||
// "this server supports the streaming (awaitable) hello protocol" and
|
||||
// switches monitoring to an exhaust hello: it sends one hello with
|
||||
// maxAwaitTimeMS and the OP_MSG exhaustAllowed flag, then expects a
|
||||
// stream of unsolicited replies each carrying moreToCome. We answer
|
||||
// once with moreToCome clear and go back to reading, so the driver
|
||||
// fails the heartbeat ("Server ended moreToCome unexpectedly"), drops
|
||||
// the connection and resets its pool — a connect/disconnect loop once
|
||||
// per heartbeat, which is what MongoDB Compass showed. Omitting the
|
||||
// field keeps monitoring on the polling path, which we do implement,
|
||||
// and matches maxWireVersion 8: streaming hello arrived in wire 9.
|
||||
}
|
||||
|
||||
fn cmd_hello(ctx: *Context, _: *wire.Message, reply: *wire.Reply) !void {
|
||||
@@ -156,7 +162,7 @@ fn cmd_ping(_: *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("gitVersion", .{ .string = "mongo-light" });
|
||||
try reply.put("gitVersion", .{ .string = "mongo-lite" });
|
||||
try reply.put("versionArray", .{ .array = try int_array(reply, &.{ 4, 4, 0, 0 }) });
|
||||
try reply.put("openssl", .{ .doc = &.{} });
|
||||
try reply.put("loaderFlags", .{ .string = "" });
|
||||
@@ -213,7 +219,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 {
|
||||
const argv = try reply.arena_alloc().alloc(bson.Pair, 2);
|
||||
argv[0] = .{ .key = "dbpath", .value = .{ .string = "mongo-light.log" } };
|
||||
argv[0] = .{ .key = "dbpath", .value = .{ .string = "mongo-lite.log" } };
|
||||
argv[1] = .{ .key = "port", .value = .{ .int32 = 27017 } };
|
||||
try reply.put("argv", .{ .array = &.{} });
|
||||
try reply.put("parsed", .{ .doc = argv });
|
||||
@@ -1363,6 +1369,28 @@ test "ping and hello replies parse" {
|
||||
try testing.expectEqual(@as(i32, 8), bson.get_pair(reply2.pairs.items, "maxWireVersion").?.int32);
|
||||
}
|
||||
|
||||
test "handshake does not advertise the streaming hello protocol" {
|
||||
// `topologyVersion` in a hello/isMaster reply tells the driver it may
|
||||
// monitor with an exhaust hello and expect moreToCome replies we never
|
||||
// send; the driver then kills the connection every heartbeat.
|
||||
var threaded: std.Io.Threaded = .init_single_threaded;
|
||||
defer threaded.deinit();
|
||||
const io = threaded.io();
|
||||
var tdb = try TestDb.init(io);
|
||||
defer tdb.deinit();
|
||||
var ctx = tdb.ctx(io);
|
||||
|
||||
for ([_][]const u8{ "hello", "isMaster", "ismaster" }) |cmd| {
|
||||
var reply = wire.Reply.init(testing.allocator);
|
||||
defer reply.deinit();
|
||||
var msg = try parse_fake_msg(cmd, .null, &.{});
|
||||
defer msg.deinit();
|
||||
try dispatch(&ctx, &msg, &reply);
|
||||
try testing.expectEqual(@as(f64, 1.0), bson.get_pair(reply.pairs.items, "ok").?.double);
|
||||
try testing.expect(bson.get_pair(reply.pairs.items, "topologyVersion") == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "unknown command gives CommandNotFound" {
|
||||
var threaded: std.Io.Threaded = .init_single_threaded;
|
||||
defer threaded.deinit();
|
||||
|
||||
Reference in New Issue
Block a user