mongo/jstests/replsets/v2_delta_oplog_entries.js

341 lines
13 KiB
JavaScript

/**
* Tests use of $v: 2 delta style oplog entries for pipeline based updates. This test only checks
* steady-state replication cases. It does not attempt to target cases where delta entries are
* re-applied as part of initial sync or rollback.
*
* This test relies on the DBHash checker to run at the end to ensure that the primaries and
* secondaries have the same data. For that reason it's important that this test not drop
* intermediate collections.
*/
import {ReplSetTest} from "jstests/libs/replsettest.js";
const rst = new ReplSetTest({name: "v2_delta_oplog_entries", nodes: 2});
rst.startSet();
rst.initiate();
const primary = rst.getPrimary();
const primaryColl = primary.getDB("test").coll;
const secondary = rst.getSecondary();
const secondaryColl = secondary.getDB("test").coll;
// Used for padding documents, in order to make full replacements expensive.
const kGiantStr = "_".repeat(100);
const kMediumLengthStr = "zzzzzzzzzzzzzzzzzzzzzzzzzz";
// Create indexes on all the fields used through out the tests, so we can verify that the index
// data is correctly updated on all the nodes.
primaryColl.createIndex({padding: 1, "x.1": 1, "a.1": -1, "subObj.a": 1});
let idGenGlob = 0;
function generateId() {
return idGenGlob++;
}
const kExpectDeltaEntry = "expectDelta";
const kExpectReplacementEntry = "expectReplacement";
// Indicates that the update ran was a noop and we should not expect to see a 'u' oplog
// entry.
const kExpectNoUpdateEntry = "expectNoEntry";
/**
* Given a connection to a node, check that the most recent oplog entry for document with
* 'expectedId' matches the type 'expectedOplogEntryType'.
*/
function checkOplogEntry(node, expectedOplogEntryType, expectedId) {
const oplog = node.getDB("local").getCollection("oplog.rs");
const res = oplog
.find({
$and: [{ns: primaryColl.getFullName()}, {$or: [{"o._id": expectedId}, {"o2._id": expectedId}]}],
})
.limit(1)
.hint({$natural: -1}) // Reverse scan, so we get the most recent entry.
.toArray();
assert.eq(res.length, 1);
const oplogEntry = res[0];
if (expectedOplogEntryType === kExpectDeltaEntry) {
assert.eq(oplogEntry.op, "u");
assert.eq(oplogEntry.o.$v, 2, oplogEntry);
assert.eq(typeof oplogEntry.o.diff, "object", oplogEntry);
// Check that the oplog entry's _id field is for the document we updated.
assert.eq(oplogEntry.o2._id, expectedId);
// Do some cursory/weak checks about the format of the 'o' field.
assert.eq(Object.keys(oplogEntry.o), ["$v", "diff"]);
for (let key of Object.keys(oplogEntry.o.diff)) {
assert.contains(key[0], ["i", "u", "s", "d"]);
}
} else if (expectedOplogEntryType === kExpectReplacementEntry) {
assert.eq(oplogEntry.op, "u");
assert.eq(oplogEntry.o.hasOwnProperty("$v"), false, oplogEntry);
} else if (expectedOplogEntryType == kExpectNoUpdateEntry) {
assert.eq(oplogEntry.op, "i");
assert.eq(oplogEntry.o._id, expectedId);
}
}
// Last parameter is whether we expect the oplog entry to only record an update rather than
// replacement.
function testUpdateReplicates({preImage, pipeline, postImage, expectedOplogEntry}) {
const idKey = preImage._id;
assert.commandWorked(primaryColl.insert(preImage));
assert.commandWorked(primaryColl.update({_id: idKey}, pipeline));
rst.awaitReplication();
const secondaryDoc = secondaryColl.findOne({_id: idKey});
assert.eq(postImage, secondaryDoc);
checkOplogEntry(primary, expectedOplogEntry, preImage._id);
}
const oplog = primary.getDB("local").getCollection("oplog.rs");
let id;
// Removing fields.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: 3, y: 3, padding: kGiantStr},
pipeline: [{$unset: ["x", "y"]}],
postImage: {_id: id, padding: kGiantStr},
expectedOplogEntry: kExpectDeltaEntry,
});
// Adding a field and updating an existing one.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "notSoLargeString", y: 0},
pipeline: [{$set: {a: "foo", y: 999}}],
postImage: {_id: id, x: "notSoLargeString", y: 999, a: "foo"},
expectedOplogEntry: kExpectDeltaEntry,
});
// Updating a subfield to a string.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "notSoLargeString", subObj: {a: 1, b: 2}},
pipeline: [{$set: {"subObj.a": "foo", y: 1}}],
postImage: {_id: id, x: "notSoLargeString", subObj: {a: "foo", b: 2}, y: 1},
expectedOplogEntry: kExpectDeltaEntry,
});
// Updating a subfield to have the same value but different type. This is designed to check that the
// server uses strict binary comparison to determine whether a field needs to be updated, rather
// than a weak BSON type insensitive comparison.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "notSoLargeString", subObj: {a: NumberLong(1), b: 2}},
pipeline: [{$set: {"subObj.a": 1, y: 1}}],
postImage: {_id: id, x: "notSoLargeString", subObj: {a: 1, b: 2}, y: 1},
expectedOplogEntry: kExpectDeltaEntry,
});
// Update a subfield to an object.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "notSoLargeString", subObj: {a: NumberLong(1), b: 2}},
pipeline: [{$set: {"subObj.a": {$const: {newObj: {subField: 1}}}, y: 1}}],
postImage: {_id: id, x: "notSoLargeString", subObj: {a: {newObj: {subField: 1}}, b: 2}, y: 1},
expectedOplogEntry: kExpectDeltaEntry,
});
// Adding a field to a sub object.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, subObj: {a: 1, b: 2}},
pipeline: [{$set: {"subObj.c": "foo"}}],
postImage: {_id: id, subObj: {a: 1, b: 2, c: "foo"}},
expectedOplogEntry: kExpectDeltaEntry,
});
// Adding a field to a sub object while removing a top level field.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, subObj: {a: 1, b: 2}, toRemove: "foo", padding: kGiantStr},
pipeline: [{$project: {subObj: 1, padding: 1}}, {$set: {"subObj.c": "foo"}}],
postImage: {_id: id, subObj: {a: 1, b: 2, c: "foo"}, padding: kGiantStr},
expectedOplogEntry: kExpectDeltaEntry,
});
// Dropping a field via inclusion projection.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, toRemove: "foo", subObj: {a: 1, b: 2}},
pipeline: [{$project: {subObj: 1}}],
postImage: {_id: id, subObj: {a: 1, b: 2}},
expectedOplogEntry: kExpectDeltaEntry,
});
// Inclusion projection dropping a subfield (subObj.toRemove).
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "foo", subObj: {a: 1, toRemove: 2}, padding: kGiantStr},
pipeline: [{$project: {subObj: {a: 1}, padding: 1}}],
postImage: {_id: id, subObj: {a: 1}, padding: kGiantStr},
expectedOplogEntry: kExpectDeltaEntry,
});
// $replaceRoot with identical document. We should expect no update oplog entry in this case.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "foo", subObj: {a: 1, b: 2}},
pipeline: [{$replaceRoot: {newRoot: {_id: id, x: "foo", subObj: {a: 1, b: 2}}}}],
postImage: {_id: id, x: "foo", subObj: {a: 1, b: 2}},
expectedOplogEntry: kExpectNoUpdateEntry,
});
// $replaceRoot with a similar document. In this case the diff should be small enough to use
// delta oplog entries instead of doing a full replacement.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "foo", subObj: {a: 1, b: 2}, padding: kGiantStr},
pipeline: [{$replaceRoot: {newRoot: {x: "bar", subObj: {a: 1, b: 2}, padding: kGiantStr}}}],
postImage: {_id: id, x: "bar", subObj: {a: 1, b: 2}, padding: kGiantStr},
expectedOplogEntry: kExpectDeltaEntry,
});
// Replace root with a very different document. In this case we should fall back to a replacement
// style update.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "foo", subObj: {a: 1, b: 2}},
pipeline: [{$replaceRoot: {newRoot: {_id: id, newField: kMediumLengthStr}}}],
postImage: {_id: id, newField: kMediumLengthStr},
expectedOplogEntry: kExpectReplacementEntry,
});
// Combine updates to existing fields and insertions of new fields.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: 1, b: {c: 2, d: {e: 3, f: 6}}, z: 3},
pipeline: [{$unset: ["b.d.f"]}, {$set: {"b.a": 5, "b.b": 3, "b.c": 2, "b.d.d": 2, "b.d.e": 10, z: 7}}],
postImage: {_id: id, padding: kGiantStr, a: 1, b: {c: 2, d: {e: 10, d: 2}, a: 5, b: 3}, z: 7},
expectedOplogEntry: kExpectDeltaEntry,
});
// Setting a sub object inside an array.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, arrField: [{x: 1}, {x: 2}]},
pipeline: [{$set: {"arrField.x": 5}}],
postImage: {_id: id, padding: kGiantStr, arrField: [{x: 5}, {x: 5}]},
expectedOplogEntry: kExpectDeltaEntry,
});
// Reordering fields with replaceRoot.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, x: "foo", y: "bar", z: "baz"},
pipeline: [{$replaceRoot: {newRoot: {_id: id, z: "baz", y: "bar", x: "foo"}}}],
postImage: {_id: id, z: "baz", y: "bar", x: "foo"},
expectedOplogEntry: kExpectDeltaEntry,
});
// Reordering two small fields in a very large document.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, x: "foo", y: "bar"},
pipeline: [{$replaceRoot: {newRoot: {_id: id, padding: kGiantStr, y: "bar", x: "foo"}}}],
postImage: {_id: id, padding: kGiantStr, y: "bar", x: "foo"},
expectedOplogEntry: kExpectDeltaEntry,
});
// Similar case of reordering fields.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: 1, b: 1, c: 1, d: 1},
pipeline: [{$replaceRoot: {newRoot: {_id: id, padding: kGiantStr, a: 1, c: 1, b: 1, d: 1}}}],
postImage: {_id: id, padding: kGiantStr, a: 1, c: 1, b: 1, d: 1},
expectedOplogEntry: kExpectDeltaEntry,
});
// Modify an element in the middle of an array.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: [1, 2, 3, 4, 5]},
pipeline: [{$set: {a: [1, 2, 999, 4, 5]}}],
postImage: {_id: id, padding: kGiantStr, a: [1, 2, 999, 4, 5]},
expectedOplogEntry: kExpectDeltaEntry,
});
// Modify an object inside an array.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: [1, 2, 3, {b: 1}, 5]},
pipeline: [{$set: {a: [1, 2, 3, {b: 2}, 5]}}],
postImage: {_id: id, padding: kGiantStr, a: [1, 2, 3, {b: 2}, 5]},
expectedOplogEntry: kExpectDeltaEntry,
});
// Object inside an array inside an object inside an array.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: [1, 2, 3, {b: [{c: 1}]}, 5]},
pipeline: [{$set: {a: [1, 2, 3, {b: [{c: 999}]}, 5]}}],
postImage: {_id: id, padding: kGiantStr, a: [1, 2, 3, {b: [{c: 999}]}, 5]},
expectedOplogEntry: kExpectDeltaEntry,
});
// Case where we append to an array.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: [1, 2, 3]},
pipeline: [{$set: {a: [1, 2, 3, 4, 5]}}],
postImage: {_id: id, padding: kGiantStr, a: [1, 2, 3, 4, 5]},
expectedOplogEntry: kExpectDeltaEntry,
});
// Case where we make an array shorter.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: [1, 2, 3]},
pipeline: [{$set: {a: [1, 2]}}],
postImage: {_id: id, padding: kGiantStr, a: [1, 2]},
expectedOplogEntry: kExpectDeltaEntry,
});
// Change element of array AND shorten it
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: [1, {b: 10}, 3]},
pipeline: [{$set: {a: [1, {b: 9}]}}],
postImage: {_id: id, padding: kGiantStr, a: [1, {b: 9}]},
expectedOplogEntry: kExpectDeltaEntry,
});
// Remove element from the middle of an array. Should still use a delta, and only rewrite the last
// parts of the array.
id = generateId();
testUpdateReplicates({
preImage: {_id: id, padding: kGiantStr, a: [1, 2, 999, 3, 4]},
pipeline: [{$set: {a: [1, 2, 3, 4]}}],
postImage: {_id: id, padding: kGiantStr, a: [1, 2, 3, 4]},
expectedOplogEntry: kExpectDeltaEntry,
});
function generateDeepObj(depth, maxDepth, value) {
return {
"padding": kGiantStr,
"subObj": depth >= maxDepth ? value : generateDeepObj(depth + 1, maxDepth, value),
};
}
// Verify that diffing the deepest objects allowed by the JS client can produce delta op-log
// entries.
id = generateId();
let path = "subObj.".repeat(146) + "subObj";
testUpdateReplicates({
preImage: Object.assign({_id: id}, generateDeepObj(1, 147, 1)),
pipeline: [{$set: {[path]: 2}}],
postImage: Object.assign({_id: id}, generateDeepObj(1, 147, 2)),
expectedOplogEntry: kExpectDeltaEntry,
});
// Don't drop any collections. At the end we want the DBHash checker will make sure there's no
// corruption.
rst.stopSet();