Files
mongo/jstests/sharding/change_stream_error_label.js

155 lines
6.7 KiB
JavaScript

/**
* Test that a change stream pipeline which encounters a retryable exception responds to the client
* with an error object that includes the "ResumableChangeStreamError" label.
* @tags: [
* requires_sharding,
* uses_change_streams,
* ]
*/
(function() {
"use strict";
// Skip cross-cluster consistency checks, since this test prematurely shuts down a shard.
TestData.skipCheckingUUIDsConsistentAcrossCluster = true;
TestData.skipCheckingIndexesConsistentAcrossCluster = true;
// Create a two-shard cluster so that we can stop one shard to test connection interruptions.
const st = new ShardingTest({
shards: 2,
mongos: 1,
rs: {nodes: 1, setParameter: {writePeriodicNoops: true, periodicNoopIntervalSecs: 1}}
});
const testDB = st.s.getDB(jsTestName());
const coll = testDB.test;
// The set of errors which might be thrown if we attempt to getMore after stopping a shard.
const expectedStopShardErrors = [
ErrorCodes.HostUnreachable,
ErrorCodes.HostNotFound,
ErrorCodes.NetworkTimeout,
ErrorCodes.SocketException,
ErrorCodes.ShutdownInProgress,
ErrorCodes.PrimarySteppedDown,
ErrorCodes.NotWritablePrimary,
ErrorCodes.InterruptedAtShutdown,
ErrorCodes.InterruptedDueToReplStateChange,
ErrorCodes.NotPrimaryNoSecondaryOk,
ErrorCodes.NotPrimaryOrSecondary
];
// First, verify that the 'failGetMoreAfterCursorCheckout' failpoint can effectively exercise the
// error label generation logic for change stream getMores.
function testFailGetMoreAfterCursorCheckoutFailpoint({mongos, errorCode, expectedLabel}) {
errorCode = ErrorCodes.doMongosRewrite(mongos, errorCode);
// Activate the failpoint and set the exception that it will throw.
assert.commandWorked(testDB.adminCommand({
configureFailPoint: "failGetMoreAfterCursorCheckout",
mode: "alwaysOn",
data: {"errorCode": errorCode}
}));
// Now open a valid $changeStream cursor...
const aggCmdRes = assert.commandWorked(
coll.runCommand("aggregate", {pipeline: [{$changeStream: {}}], cursor: {}}));
// ... run a getMore using the cursorID from the original command response, and confirm that the
// expected error was thrown...
const getMoreRes = assert.commandFailedWithCode(
testDB.runCommand({getMore: aggCmdRes.cursor.id, collection: coll.getName()}), errorCode);
/// ... and confirm that the label is present or absent depending on the "expectedLabel" value.
const errorLabels = (getMoreRes.errorLabels || []);
assert.eq("errorLabels" in getMoreRes, expectedLabel, getMoreRes);
assert.eq(errorLabels.includes("ResumableChangeStreamError"), expectedLabel, getMoreRes);
// Finally, disable the failpoint.
assert.commandWorked(
testDB.adminCommand({configureFailPoint: "failGetMoreAfterCursorCheckout", mode: "off"}));
}
// Test the expected output for both resumable and non-resumable error codes.
testFailGetMoreAfterCursorCheckoutFailpoint(
{mongos: st.s, errorCode: ErrorCodes.ShutdownInProgress, expectedLabel: true});
testFailGetMoreAfterCursorCheckoutFailpoint(
{mongos: st.s, errorCode: ErrorCodes.FailedToParse, expectedLabel: false});
// Now test both aggregate and getMore under conditions of an actual cluster outage. Shard the
// collection on shard0, split at {_id: 0}, and move the upper chunk to the other shard.
assert.commandWorked(st.s.adminCommand({enableSharding: testDB.getName()}));
st.ensurePrimaryShard(testDB.getName(), st.shard0.shardName);
st.shardColl(coll, {_id: 1}, {_id: 0}, {_id: 0});
// Open a change stream on the collection...
const csCursor = coll.watch([]);
// ... insert some documents, ensuring that they are split across both shards...
for (let i = 1; i <= 10; ++i) {
assert.commandWorked(coll.insert([{_id: -i}, {_id: i}]));
}
// ... and read all the corresponding events from the stream.
assert.soon(() => {
return (csCursor.hasNext() && csCursor.next().documentKey._id === 10);
});
// Issue a "find" query to retrieve the first few documents, leaving the cursor open.
const findCursor = coll.find({}).sort({_id: 1}).batchSize(2);
assert.docEq({_id: -10}, findCursor.next());
assert.docEq({_id: -9}, findCursor.next());
assert.eq(findCursor.objsLeftInBatch(), 0);
// Open a non-$changeStream agg cursor. Set the batchSize to 0, since otherwise the aggregation will
// pull all documents from the shards at once and cache them on mongoS, meaning that the subsequent
// getMore will not attempt to contact a shard and will not throw the expected network exception.
const aggCursor = coll.aggregate([{$match: {}}, {$sort: {_id: 1}}], {cursor: {batchSize: 0}});
// Now stop shard0...
st.rs0.stopSet();
// ... and confirm that getMore on the $changeStream throws one of the expected exceptions.
let err = assert.throws(() => assert.soon(() => csCursor.hasNext()));
assert.commandFailedWithCode(err, expectedStopShardErrors);
// Confirm that the response includes the "ResumableChangeStreamError" error label.
assert("errorLabels" in err, err);
assert.contains("ResumableChangeStreamError", err.errorLabels, err);
// Confirm that getMore on the find cursor throws the same exception...
err = assert.throws(() => assert.soon(() => findCursor.hasNext()));
assert.commandFailedWithCode(err, expectedStopShardErrors);
// ... but does NOT have the "ResumableChangeStreamError" error label.
assert(!("errorLabels" in err), err);
// Confirm that getMore on the non-$changeStream agg cursor throws the same exception...
err = assert.throws(() => assert.soon(() => aggCursor.hasNext()));
assert.commandFailedWithCode(err, expectedStopShardErrors);
// ... but does NOT have the "ResumableChangeStreamError" error label.
assert(!("errorLabels" in err), err);
// Now confirm that attempting to open a new stream fails on the initial aggregate.
err = assert.throws(() => coll.watch([]));
assert.commandFailedWithCode(err, ErrorCodes.FailedToSatisfyReadPreference);
// Confirm that the response includes the "ResumableChangeStreamError" error label.
assert("errorLabels" in err, err);
assert.contains("ResumableChangeStreamError", err.errorLabels, err);
// Attempting to issue a non-$changeStream aggregation also fails...
err = assert.throws(() => coll.aggregate([{$match: {}}, {$sort: {_id: 1}}]).itcount());
assert.commandFailedWithCode(err, ErrorCodes.FailedToSatisfyReadPreference);
//... but does NOT include the "ResumableChangeStreamError" error label.
assert(!("errorLabels" in err), err);
// Attempting to issue a new "find" query also fails...
err = assert.throws(() => coll.find({}).sort({_id: 1}).itcount());
assert.commandFailedWithCode(err, ErrorCodes.FailedToSatisfyReadPreference);
// ... but does NOT include the "ResumableChangeStreamError" error label.
assert(!("errorLabels" in err), err);
st.stop();
}());