Files
mongo/jstests/replsets/oplog_format.js
Justin Seyster 390e5f47f0 SERVER-30705 Add $v field for update semantics in oplog updates.
With the new UpdateNodes class hierarchy, there are two code paths for
applying an update to a document that have slightly different
semantics. The order of fields in the resulting document can vary
depending on which code path is used to apply an update. A difference
in ordering between documents in a replica set is considered a
"mismatch," so we need to ensure that secondaries always apply updates
using the same update system that the primary uses.

When an update executes as part of the application of an oplog entry,
the update is now allowed to have a $v field, which allows it to
specify which semantics were used by the operation that we are
replicating by applying the entry.  When the primary uses the new
semantics (because it is a 3.6 mongod with featureCompatibilityVersion
set to 3.6), it includes {$v: 1} in the oplog's update document to
indicate that the secondary should apply with the newer 'UpdateNode'
semantics.

There are two other places where we need this behavior:
  1) In role_graph_update.cpp, where the handleOplogUpdate observer
  needs to update its in-memory BSON representation of a role to
  reflect an update in the admin database and
  2) in the applyOps command, which is used for testing how oplog
  entries get applied.

Both these code paths set the fromOplogApplication flag, which
replaces the old fromReplication flag, and they also gain behavior
that used to be exclusive to oplog applications from
replication. (Specifically, they skip update validation checks, which
should have already passed before the oplog entry was created.)
2017-09-14 12:14:54 -04:00

179 lines
8.0 KiB
JavaScript

/**
* These tests verify that the oplog entries are created correctly for updates
*
* Do not add more tests here but instead add C++ unit tests in db/ops/modifier*_test files
*
*/
"use strict";
var replTest = new ReplSetTest({nodes: 1, oplogSize: 2, nodeOptions: {smallfiles: ""}});
var nodes = replTest.startSet();
replTest.initiate();
var master = replTest.getPrimary();
var coll = master.getDB("o").fake;
var cdb = coll.getDB();
var assertLastOplog = function(o, o2, msg) {
var last = master.getDB("local").oplog.rs.find().limit(1).sort({$natural: -1}).next();
assert.eq(last.ns, coll.getFullName(), "ns bad : " + msg);
assert.docEq(last.o, o, "o bad : " + msg);
if (o2)
assert.docEq(last.o2, o2, "o2 bad : " + msg);
return last.ts;
};
// set things up.
coll.save({_id: 1});
assertLastOplog({_id: 1}, null, "save -- setup ");
/**
* The first ones are from the old updatetests which tested the internal impl using a modSet
*/
var msg = "IncRewriteExistingField: $inc $set";
coll.save({_id: 1, a: 2});
assertLastOplog({_id: 1, a: 2}, {_id: 1}, "save " + msg);
var res = assert.writeOK(coll.update({}, {$inc: {a: 1}, $set: {b: 2}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: 3, b: 2}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {a: 3, b: 2}}, {_id: 1}, msg);
var msg = "IncRewriteNonExistingField: $inc $set";
coll.save({_id: 1, c: 0});
assertLastOplog({_id: 1, c: 0}, {_id: 1}, "save " + msg);
res = assert.writeOK(coll.update({}, {$inc: {a: 1}, $set: {b: 2}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, c: 0, a: 1, b: 2}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {a: 1, b: 2}}, {_id: 1}, msg);
var msg = "TwoNestedPulls: two $pull";
coll.save({_id: 1, a: {b: [1, 2], c: [1, 2]}});
assertLastOplog({_id: 1, a: {b: [1, 2], c: [1, 2]}}, {_id: 1}, "save " + msg);
res = assert.writeOK(coll.update({}, {$pull: {'a.b': 2, 'a.c': 2}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: {b: [1], c: [1]}}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {'a.b': [1], 'a.c': [1]}}, {_id: 1}, msg);
var msg = "MultiSets: two $set";
coll.save({_id: 1, a: 1, b: 1});
assertLastOplog({_id: 1, a: 1, b: 1}, {_id: 1}, "save " + msg);
res = assert.writeOK(coll.update({}, {$set: {a: 2, b: 2}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: 2, b: 2}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {a: 2, b: 2}}, {_id: 1}, msg);
// More tests to validate the oplog format and correct excution
var msg = "bad single $set";
coll.save({_id: 1, a: 1});
assertLastOplog({_id: 1, a: 1}, {_id: 1}, "save " + msg);
res = assert.writeOK(coll.update({}, {$set: {a: 2}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: 2}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {a: 2}}, {_id: 1}, msg);
var msg = "bad single $inc";
res = assert.writeOK(coll.update({}, {$inc: {a: 1}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: 3}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {a: 3}}, {_id: 1}, msg);
var msg = "bad double $set";
res = assert.writeOK(coll.update({}, {$set: {a: 2, b: 2}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: 2, b: 2}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {a: 2, b: 2}}, {_id: 1}, msg);
var msg = "bad save";
assert.writeOK(coll.save({_id: 1, a: [2]}));
assert.docEq({_id: 1, a: [2]}, coll.findOne({}), msg);
assertLastOplog({_id: 1, a: [2]}, {_id: 1}, msg);
var msg = "bad array $inc";
res = assert.writeOK(coll.update({}, {$inc: {"a.0": 1}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: [3]}, coll.findOne({}), msg);
var lastTS = assertLastOplog({$v: 1, $set: {"a.0": 3}}, {_id: 1}, msg);
var msg = "bad $setOnInsert";
res = assert.writeOK(coll.update({}, {$setOnInsert: {a: -1}}));
assert.eq(res.nMatched, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: [3]}, coll.findOne({}), msg); // No-op
var otherTS = assertLastOplog({$v: 1, $set: {"a.0": 3}}, {_id: 1}, msg); // Nothing new
assert.eq(lastTS, otherTS, "new oplog was not expected -- " + msg); // No new oplog entry
coll.remove({});
assert.eq(coll.find().itcount(), 0, "collection not empty");
var msg = "bad $setOnInsert w/upsert";
res = assert.writeOK(coll.update({}, {$setOnInsert: {a: 200}}, {upsert: true})); // upsert
assert.eq(res.nUpserted, 1, "update failed for '" + msg + "': " + res.toString());
var id = res.getUpsertedId()._id;
assert.docEq({_id: id, a: 200}, coll.findOne({}), msg); // No-op
assertLastOplog({_id: id, a: 200}, null, msg); // No new oplog entry
coll.remove({});
assert.eq(coll.find().itcount(), 0, "collection not empty-2");
/* inconsistent oplog format with old code -- new is okay but less efficient
* enable once we switch the default
var msg = "bad array $push";
coll.save({_id:1, a:[1,2]})
coll.update({}, {$push:{a:3}});
var gle = cdb.getLastErrorObj();
assert.isnull(gle.err, msg);
assert.eq(gle.n, 1, "update failed for '" + msg +"': "+ tojson(gle));
assert.docEq({_id:1, a:[1,2,3]}, coll.findOne({}), msg);
//assertLastOplog({$set:{"a.2": 3}}, {_id:1}, msg); // old format
assertLastOplog({$set:{"a": [1,2,3]}}, {_id:1}, msg); // new format
*/
var msg = "bad array $push 2";
coll.save({_id: 1, a: "foo"});
res = assert.writeOK(coll.update({}, {$push: {c: 18}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: "foo", c: [18]}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {"c": [18]}}, {_id: 1}, msg);
var msg = "bad array $push $slice";
coll.save({_id: 1, a: {b: [18]}});
res = assert.writeOK(coll.update({_id: {$gt: 0}}, {$push: {"a.b": {$each: [1, 2], $slice: -2}}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: {b: [1, 2]}}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {"a.b": [1, 2]}}, {_id: 1}, msg);
var msg = "bad array $push $sort ($slice -100)";
coll.save({_id: 1, a: {b: [{c: 2}, {c: 1}]}});
res = assert.writeOK(
coll.update({}, {$push: {"a.b": {$each: [{c: -1}], $sort: {c: 1}, $slice: -100}}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: {b: [{c: -1}, {c: 1}, {c: 2}]}}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {"a.b": [{c: -1}, {c: 1}, {c: 2}]}}, {_id: 1}, msg);
var msg = "bad array $push $slice $sort";
coll.save({_id: 1, a: [{b: 2}, {b: 1}]});
res = assert.writeOK(
coll.update({_id: {$gt: 0}}, {$push: {a: {$each: [{b: -1}], $slice: -2, $sort: {b: 1}}}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: [{b: 1}, {b: 2}]}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {a: [{b: 1}, {b: 2}]}}, {_id: 1}, msg);
var msg = "bad array $push $slice $sort first two";
coll.save({_id: 1, a: {b: [{c: 2}, {c: 1}]}});
res = assert.writeOK(
coll.update({_id: {$gt: 0}}, {$push: {"a.b": {$each: [{c: -1}], $slice: -2, $sort: {c: 1}}}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: {b: [{c: 1}, {c: 2}]}}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {"a.b": [{c: 1}, {c: 2}]}}, {_id: 1}, msg);
var msg = "bad array $push $slice $sort reversed first two";
coll.save({_id: 1, a: {b: [{c: 1}, {c: 2}]}});
res = assert.writeOK(
coll.update({_id: {$gt: 0}}, {$push: {"a.b": {$each: [{c: -1}], $slice: -2, $sort: {c: -1}}}}));
assert.eq(res.nModified, 1, "update failed for '" + msg + "': " + res.toString());
assert.docEq({_id: 1, a: {b: [{c: 1}, {c: -1}]}}, coll.findOne({}), msg);
assertLastOplog({$v: 1, $set: {"a.b": [{c: 1}, {c: -1}]}}, {_id: 1}, msg);
replTest.stopSet();