0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-11-27 23:27:11 +01:00
mongodb/jstests/change_streams/projection_fakes_internal_event.js
Matt Broadstone 771dabd098 SERVER-81339 Convert ReplSetTest and ShardingTest to modules (#26332)
GitOrigin-RevId: 744aa110a53786b23c62ff53f87a1418b5991e8d
2024-08-20 22:00:49 +00:00

216 lines
8.6 KiB
JavaScript

/**
* Tests that a user projection which fakes an internal topology-change event is handled gracefully
* in a sharded cluster.
* TODO SERVER-65778: rework this test when we can handle faked internal events more robustly.
*
* Tests that if a user fakes an internal event with a projection nothing crashes, so not valuable
* to test with a config shard.
* @tags: [assumes_read_preference_unchanged, config_shard_incompatible]
*/
import {ShardingTest} from "jstests/libs/shardingtest.js";
const numShards = 2;
const st = new ShardingTest({
shards: numShards,
rs: {nodes: 1, setParameter: {writePeriodicNoops: true, periodicNoopIntervalSecs: 1}}
});
const mongosConn = st.s;
const testDB = mongosConn.getDB(jsTestName());
const adminDB = mongosConn.getDB("admin");
const testColl = testDB.test;
// Insert one test document that points to a valid shard, and one that points to an invalid shard.
// These will generate change events that look identical to a config.shards entry, except for 'ns'.
// It also means that the documentKey field in the resume token will look like a potentially valid
// new-shard document.
const existingShardDoc = testDB.getSiblingDB("config").shards.find({_id: st.rs0.name}).next();
const existingShardWrongNameDoc = {
_id: "nonExistentName",
host: existingShardDoc.host
};
const existingShardWrongHostDoc = {
_id: st.rs1.name,
host: `${st.rs1.name}/${st.rs1.host}-wrong:${st.rs1.ports[0]}`
};
const fakeShardDoc = {
_id: "shardX",
host: "shardX/nonExistentHost:27017"
};
const invalidShardDoc = {
_id: "shardY",
host: null
};
const configDotShardsNs = {
db: "config",
coll: "shards"
};
assert.commandWorked(testColl.insert(existingShardWrongNameDoc));
assert.commandWorked(testColl.insert(existingShardWrongHostDoc));
assert.commandWorked(testColl.insert(existingShardDoc));
assert.commandWorked(testColl.insert(invalidShardDoc));
assert.commandWorked(testColl.insert(fakeShardDoc));
// Log the shard description documents that we just inserted into the collection.
jsTestLog("Shard docs: " + tojson(testColl.find().toArray()));
// Helper function which opens a stream with the given projection and asserts that its behaviour
// conforms to the specified arguments; it will either throw the given error code, or return the
// expected events. Passing an empty array will confirm that we see no events in the stream. We
// further confirm that the faked events do not cause additional cursors to be opened.
function assertChangeStreamBehaviour(projection, expectedEvents, expectedErrorCode = null) {
// Can't expect both to see events and to throw an exception.
assert(!(expectedEvents && expectedErrorCode));
// Generate a random ID for this stream.
const commentID = `${Math.random()}`;
// Create a change stream cursor with the specified projection.
var csCursor = testColl.watch([{$addFields: projection}],
{startAtOperationTime: Timestamp(1, 1), comment: commentID});
// Confirm that the observed events match the expected events, if specified.
if (expectedEvents && expectedEvents.length > 0) {
for (let expectedEvent of expectedEvents) {
assert.soon(() => csCursor.hasNext());
const nextEvent = csCursor.next();
for (let fieldName in expectedEvent) {
assert.eq(
expectedEvent[fieldName], nextEvent[fieldName], {expectedEvent, nextEvent});
}
}
}
// If there are no expected events, confirm that the token advances without seeing anything.
if (expectedEvents && expectedEvents.length == 0) {
const startPoint = csCursor.getResumeToken();
assert.soon(() => {
assert(!csCursor.hasNext(), () => tojson(csCursor.next()));
return bsonWoCompare(csCursor.getResumeToken(), startPoint) > 0;
});
}
// If we expect an error code, assert that we throw it soon.
if (expectedErrorCode) {
assert.soon(() => {
try {
assert.throwsWithCode(() => csCursor.hasNext(), expectedErrorCode);
} catch (err) {
return false;
}
return true;
});
} else {
// Otherwise, confirm that we still only have a single cursor on each shard. It's possible
// that the same cursor will be listed as both active and inactive, so group by cursorId.
const openCursors = adminDB
.aggregate([
{$currentOp: {idleCursors: true}},
{$match: {"cursor.originatingCommand.comment": commentID}},
{
$group: {
_id: {shard: "$shard", cursorId: "$cursor.cursorId"},
currentOps: {$push: "$$ROOT"}
}
}
])
.toArray();
assert.eq(openCursors.length,
numShards,
// Dump all the running operations for better debuggability.
() => tojson(adminDB.aggregate([{$currentOp: {idleCursors: true}}]).toArray()));
}
// Close the change stream when we are done.
csCursor.close();
}
// Test that a projection which fakes a 'migrateChunkToNewShard' event is swallowed but has no
// effect.
let testProjection = {operationType: "migrateChunkToNewShard"};
assertChangeStreamBehaviour(testProjection, []);
// Test that a projection which fakes an event on config.shards with a non-string operationType is
// allowed to pass through.
testProjection = {
ns: configDotShardsNs,
operationType: null
};
assertChangeStreamBehaviour(testProjection, [
{operationType: null, fullDocument: existingShardWrongNameDoc},
{operationType: null, fullDocument: existingShardWrongHostDoc},
{operationType: null, fullDocument: existingShardDoc},
{operationType: null, fullDocument: invalidShardDoc},
{operationType: null, fullDocument: fakeShardDoc}
]);
// Test that a projection which fakes an event on config.shards with a non-timestamp clusterTime
// is allowed to pass through.
testProjection = {
ns: configDotShardsNs,
clusterTime: null
};
assertChangeStreamBehaviour(testProjection, [
{clusterTime: null, fullDocument: existingShardWrongNameDoc},
{clusterTime: null, fullDocument: existingShardWrongHostDoc},
{clusterTime: null, fullDocument: existingShardDoc},
{clusterTime: null, fullDocument: invalidShardDoc},
{clusterTime: null, fullDocument: fakeShardDoc}
]);
// Test that a projection which fakes an event on config.shards with a non-object fullDocument
// is allowed to pass through.
testProjection = {
ns: configDotShardsNs,
fullDocument: null
};
assertChangeStreamBehaviour(testProjection, [
{fullDocument: null},
{fullDocument: null},
{fullDocument: null},
{fullDocument: null},
{fullDocument: null}
]);
// Test that a projection which fakes a new-shard event on config.shards with a valid fullDocument
// pointing to an existing shard is swallowed but has no effect.
testProjection = {
ns: configDotShardsNs,
fullDocument: existingShardDoc
};
assertChangeStreamBehaviour(testProjection, []);
// Test that a projection which fakes a new-shard event on config.shards with a valid fullDocument
// pointing to an existing shard's host, but the wrong shard name, throws as it attempts to connect.
testProjection = {
ns: configDotShardsNs,
fullDocument: existingShardWrongNameDoc
};
assertChangeStreamBehaviour(testProjection, null, ErrorCodes.ShardNotFound);
// Test that a projection which fakes a new-shard event on config.shards with a valid fullDocument
// pointing to an existing shard's name, but the wrong host, is swallowed and has no effect.
testProjection = {
ns: configDotShardsNs,
fullDocument: existingShardWrongHostDoc
};
assertChangeStreamBehaviour(testProjection, []);
// Test that a projection which fakes a new-shard event on config.shards with a valid fullDocument
// pointing to a non-existent shard throws as it attempts to connect.
testProjection = {
ns: configDotShardsNs,
fullDocument: fakeShardDoc
};
assertChangeStreamBehaviour(testProjection, null, ErrorCodes.ShardNotFound);
// Test that a projection which fakes a new-shard event on config.shards with an invalid
// fullDocument throws a validation exception.
testProjection = {
ns: configDotShardsNs,
fullDocument: invalidShardDoc
};
assertChangeStreamBehaviour(testProjection, null, ErrorCodes.TypeMismatch);
st.stop();