0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-12-01 09:32:32 +01:00
mongodb/jstests/multiVersion/mixed_storage_version_replication.js

656 lines
23 KiB
JavaScript

/*
* Generally test that replica sets still function normally with mixed versions and mixed storage
* engines. This test will set up a replica set containing members of various versions and
* storage engines, do a bunch of random work, and assert that it replicates the same way on all
* nodes.
*/
load('jstests/libs/parallelTester.js');
// Seed random numbers and print the seed. To reproduce a failed test, look for the seed towards
// the beginning of the output, and give it as an argument to randomize.
jsTest.randomize();
/*
* Namespace for all random operation helpers. Actual tests start below
*/
var RandomOps = {
// Change this to print all operations run.
verbose: false,
// 'Random' documents will have various combinations of these names mapping to these values
fieldNames: ["a", "b", "c", "longerName", "numbered10", "dashed-name"],
fieldValues: [ true, false, 0, 44, -123, "", "String", [], [false, "x"],
["array", 1, {doc: true}, new Date().getTime()], {},
{embedded: "document", weird: ["values", 0, false]}, new Date().getTime()
],
/*
* Return a random element from Array a.
*/
randomChoice: function(a) {
if (a.length === 0) {
print("randomChoice called on empty input!");
return null;
}
var x = Random.rand();
while (x === 1.0) { // Would be out of bounds
x = Random.rand();
}
var i = Math.floor(x*a.length);
return a[i];
},
/*
* Uses above arrays to create a new doc with a random amount of fields mapping to random
* values.
*/
randomNewDoc: function() {
var doc = {};
for (var i = 0; i < Random.randInt(0,this.fieldNames.length); i++) {
doc[this.randomChoice(this.fieldNames)] = this.randomChoice(this.fieldValues);
}
return doc;
},
/*
* Returns the names of all 'user created' (non admin/local) databases which have some data in
* them, or an empty list if none exist.
*/
getCreatedDatabases: function(conn) {
var created = [];
var dbs = conn.getDBs().databases;
for (var i in dbs) {
var db = dbs[i];
if (db.name !== 'local' && db.name !== 'admin' && db.empty === false) {
created.push(db.name);
}
}
return created;
},
/*
* Return a random non-system.indexes collection from the 'user created' collections.
*/
getRandomExistingCollection: function(conn) {
var dbs = this.getCreatedDatabases(conn);
if (dbs.length === 0) { return null; }
var dbName = this.randomChoice(dbs);
var db = conn.getDB(dbName);
if (db.getCollectionNames().length <= 1) {
return null;
}
var coll = this.randomChoice(db.getCollectionNames());
while (coll == "system.indexes") {
coll = this.randomChoice(db.getCollectionNames());
}
return db[coll];
},
getRandomDoc: function(collection) {
try {
var randIndex = Random.randInt(0, collection.find().count());
return collection.find().sort({$natural: 1}).skip(randIndex).limit(1)[0];
} catch(e) {
return undefined;
}
},
/*
* Returns a random user defined collection, selecting from only those for which filterFn
* returns true, or null if there are none.
*/
getRandomCollectionWFilter: function(conn, filterFn) {
var matched = [];
var dbs = this.getCreatedDatabases(conn);
for (var i in dbs) {
var dbName = dbs[i];
var colls = conn.getDB(dbName).getCollectionNames();
for (var j in colls) {
var coll = colls[j];
if (filterFn(dbName, coll)) {
matched.push(coll);
}
}
}
if (matched.length === 0) { return null; }
return this.randomChoice(matched);
},
//////////////////////////////////////////////////////////////////////////////////
// RANDOMIZED CRUD OPERATIONS
//////////////////////////////////////////////////////////////////////////////////
/*
* Insert a random document into a random collection, with a random writeconcern
*/
insert: function(conn) {
var databases = ["tic", "tac", "toe"];
var collections = ["eeny", "meeny", "miny", "moe"];
var writeConcerns = [-1, 0, 1, 2, 3, 4, 5, 6, "majority"];
var db = this.randomChoice(databases);
var coll = this.randomChoice(collections);
var doc = this.randomNewDoc();
if (Random.rand() < 0.5) {
doc._id = new ObjectId().str; // Vary whether or not we include the _id
}
var writeConcern = this.randomChoice(writeConcerns);
var journal = this.randomChoice([true, false]);
if (this.verbose) {
print("Inserting: ");
printjson(doc);
print("With write concern: " + writeConcern + " and journal: " + journal);
}
var result = conn.getDB(db)[coll].insert(doc,
{writeConcern: {w: writeConcern},
journal: journal});
assert.eq(result.ok, 1);
if (this.verbose) { print("done."); }
},
/*
* remove a random document from a random collection
*/
remove: function(conn) {
var coll = this.getRandomExistingCollection(conn);
if (coll === null || coll.find().count() === 0) {
return null; // No data, can't delete anything.
}
var doc = this.getRandomDoc(coll);
if (doc === undefined) {
// If multithreaded, there could have been issues finding a random doc.
// If so, just skip this operation.
return;
}
if (this.verbose) {
print("Deleting:");
printjson(doc);
}
try {
coll.remove(doc);
} catch(e) {
if (this.verbose) { print("Caught exception in remove: " + e); }
}
if (this.verbose) { print("done."); }
},
/*
* Update a random document from a random collection. Set a random field to a (possibly) new
* value.
*/
update: function(conn) {
var coll = this.getRandomExistingCollection(conn);
if (coll === null || coll.find().count() === 0) {
return null; // No data, can't update anything.
}
var doc = this.getRandomDoc(coll);
if (doc === undefined) {
// If multithreaded, there could have been issues finding a random doc.
// If so, just skip this operation.
return;
}
var field = this.randomChoice(this.fieldNames);
var updateDoc = {$set: {}};
updateDoc.$set[field] = this.randomChoice(this.fieldValues);
if (this.verbose) {
print("Updating:");
printjson(doc);
print("with:");
printjson(updateDoc);
}
// If multithreaded, doc might not exist anymore.
try {
coll.update(doc, updateDoc);
} catch(e) {
if (this.verbose) { print("Caught exception in update: " + e); }
}
if (this.verbose) { print("done."); }
},
//////////////////////////////////////////////////////////////////////////////////
// RANDOMIZED COMMANDS
//////////////////////////////////////////////////////////////////////////////////
/*
* Randomly rename a collection to a new name. New name will be an ObjectId string.
*/
renameCollection: function(conn) {
var coll = this.getRandomExistingCollection(conn);
if (coll === null) { return null; }
var newName = coll.getDB() + "." + new ObjectId().str;
if (this.verbose) {
print("renaming collection " + coll.getFullName() + " to " + newName);
}
assert.commandWorked(
conn.getDB("admin").runCommand({renameCollection: coll.getFullName(), to: newName})
);
if (this.verbose) { print("done."); }
},
/*
* Randomly drop a user created database.
*/
dropDatabase: function(conn) {
var dbs = this.getCreatedDatabases(conn);
if (dbs.length === 0) { return null; }
var dbName = this.randomChoice(dbs);
if (this.verbose) {
print("Dropping database " + dbName);
}
assert.commandWorked(
conn.getDB(dbName).runCommand({dropDatabase: 1})
);
if (this.verbose) { print("done."); }
},
/*
* Randomly drop a user created collection
*/
dropCollection: function(conn) {
var coll = this.getRandomExistingCollection(conn);
if (coll === null) { return null; }
if (this.verbose) {
print("Dropping collection " + coll.getFullName());
}
assert.commandWorked(
conn.getDB(coll.getDB()).runCommand({drop: coll.getName()})
);
if (this.verbose) { print("done."); }
},
/*
* Randomly create an index on a random field in a random user defined collection.
*/
createIndex: function(conn) {
var coll = this.getRandomExistingCollection(conn);
if (coll === null) { return null; }
var index = {};
index[this.randomChoice(this.fieldNames)] = this.randomChoice([-1, 1]);
if (this.verbose) {
print("Adding index " + tojsononeline(index) + " to " + coll.getFullName());
}
coll.ensureIndex(index);
if (this.verbose) { print("done."); }
},
/*
* Randomly drop one existing index on a random user defined collection
*/
dropIndex: function(conn) {
var coll = this.getRandomExistingCollection(conn);
if (coll === null) { return null; }
var index = this.randomChoice(coll.getIndices());
if (index.name === "_id_") {
return null; // Don't drop that one.
}
if (this.verbose) {
print("Dropping index " + tojsononeline(index.key) + " from " + coll.getFullName());
}
assert.commandWorked(
coll.dropIndex(index.name)
);
if (this.verbose) { print("done."); }
},
/*
* Select a random collection and flip the user flag for usePowerOf2Sizes
*/
collMod: function(conn) {
var coll = this.getRandomExistingCollection(conn);
if (coll === null) { return null; }
var toggle = !coll.stats().userFlags;
if (this.verbose) {
print("Modifying usePowerOf2Sizes to " + toggle + " on collection " +
coll.getFullName());
}
conn.getDB(coll.getDB()).runCommand({collMod: coll.getName(), usePowerOf2Sizes: toggle});
if (this.verbose) { print("done."); }
},
/*
* Select a random user-defined collection and empty it
*/
emptyCapped: function(conn) {
var isCapped = function(dbName, coll) {
return conn.getDB(dbName)[coll].isCapped();
};
var coll = this.getRandomCollectionWFilter(conn, isCapped);
if (coll === null) { return null; }
if (this.verbose) {
print("Emptying capped collection: " + coll.getFullName());
}
assert.commandWorked(
conn.getDB(coll.getDB()).runCommand({emptycapped: coll.getName()})
);
if (this.verbose) { print("done."); }
},
/*
* Apply some ops to a random collection. For now we'll just have insert ops.
*/
applyOps: function(conn) {
// Check if there are any valid collections to choose from.
if (this.getRandomExistingCollection(conn) === null) { return null; }
var ops = [];
// Insert between 1 and 10 things.
for (var i = 0; i < Random.randInt(1, 10); i++) {
var coll = this.getRandomExistingCollection(conn);
var doc = this.randomNewDoc();
doc._id = new ObjectId();
if (coll !== null) {
ops.push({op: "i", ns: coll.getFullName(), o: doc});
}
}
if (this.verbose) {
print("Applying the following ops: ");
printjson(ops);
}
assert.commandWorked(
conn.getDB("admin").runCommand({applyOps: ops})
);
if (this.verbose) { print("done."); }
},
/*
* Create a random collection. Use an ObjectId for the name
*/
createCollection: function(conn) {
var dbs = this.getCreatedDatabases(conn);
if (dbs.length === 0) { return null; }
var dbName = this.randomChoice(dbs);
var newName = new ObjectId().str;
if (this.verbose) {
print("Creating new collection: " + "dbName" + "." + newName);
}
assert.commandWorked(
conn.getDB(dbName).runCommand({create: newName})
);
if (this.verbose) { print("done."); }
},
/*
* Convert a random non-capped collection to a capped one with size 1MB.
*/
convertToCapped: function(conn) {
var isNotCapped = function(dbName, coll) {
return conn.getDB(dbName)[coll].isCapped();
};
var coll = this.getRandomCollectionWFilter(conn, isNotCapped);
if (coll === null) { return null; }
if (this.verbose) {
print("Converting " + coll.getFullName() + " to a capped collection.");
}
assert.commandWorked(
conn.getDB(coll.getDB()).runCommand({convertToCapped: coll.getName(), size: 1024*1024})
);
if (this.verbose) { print("done."); }
},
appendOplogNote: function(conn) {
var note = "Test note " + new ObjectId().str;
if (this.verbose) {
print("Appending oplog note: " + note);
}
assert.commandWorked(
conn.getDB("admin").runCommand({appendOplogNote: note, data: {some: 'doc'}})
);
if (this.verbose) { print("done."); }
},
/*
* Repeatedly call methods numOps times, choosing randomly from possibleOps, which should be
* a list of strings representing method names of the RandomOps object.
*/
doRandomWork: function(conn, numOps, possibleOps) {
for (var i = 0; i < numOps; i++) {
op = this.randomChoice(possibleOps);
this[op](conn);
}
}
}; // End of RandomOps
//////////////////////////////////////////////////////////////////////////////////
// OTHER HELPERS
//////////////////////////////////////////////////////////////////////////////////
function isArbiter(conn) {
return conn.adminCommand({isMaster: 1}).arbiterOnly === true;
}
function removeFromArray(elem, a) {
a.splice(a.indexOf(elem), 1);
}
/*
* builds a function to be passed to assert.soon. Needs to know which node to expect as the new
* primary
*/
var primaryChanged = function(conns, replTest, primaryIndex) {
return function() {
return conns[primaryIndex] == replTest.getPrimary();
};
};
/*
* If we have determined a collection doesn't match on two hosts, use this to get a string of the
* differences.
*/
function getCollectionDiff(db1, db2, collName) {
var coll1 = db1[collName];
var coll2 = db2[collName];
var cur1 = coll1.find().sort({$natural: 1});
var cur2 = coll2.find().sort({$natural: 1});
var diffText = "";
while (cur1.hasNext() && cur2.hasNext()) {
var doc1 = cur1.next();
var doc2 = cur2.next();
if (doc1 != doc2) {
diffText += "mismatching doc:" + tojson(doc1) + tojson(doc2);
}
}
if (cur1.hasNext()) {
diffText += db1.getMongo().host + " has extra documents:";
while (cur1.hasNext()) {
diffText += "\n" + tojson(cur1.next());
}
}
if (cur2.hasNext()) {
diffText += db2.getMongo().host + " has extra documents:";
while (cur2.hasNext()) {
diffText += "\n" + tojson(cur2.next());
}
}
return diffText;
}
/*
* Check if two databases are equal. If not, print out what the differences are to aid with
* debugging.
*/
function assertDBsEq(db1, db2) {
assert.eq(db1.getName(), db2.getName());
var hash1 = db1.runCommand({dbHash: 1});
var hash2 = db2.runCommand({dbHash: 1});
var host1 = db1.getMongo().host;
var host2 = db2.getMongo().host;
var success = true;
var collNames1 = db1.getCollectionNames();
var collNames2 = db2.getCollectionNames();
var diffText = "";
if (db1.getName() === 'local') {
// We don't expect the entire local collection to be the same, not even the oplog, since
// it's a capped collection.
return;
}
else if (hash1.md5 != hash2.md5) {
for (var i = 0; i < Math.min(collNames1.length, collNames2.length); i++) {
var collName = collNames1[i];
if (hash1.collections[collName] !== hash2.collections[collName]) {
if (db1[collName].stats().capped) {
if (!db2[collName].stats().capped) {
success = false;
diffText += "\n" + collName + " is capped on " + host1 + " but not on " +
host2;
}
else {
// Skip capped collections. They are not expected to be the same from host
// to host.
continue;
}
}
else {
success = false;
diffText += "\n" + collName + " differs: " +
getCollectionDiff(db1, db2, collName);
}
}
}
}
assert.eq(success, true, "Database " + db1.getName() + " differs on " + host1 + " and " +
host2 + "\nCollections: " + collNames1 + " vs. " + collNames2 + "\n" + diffText);
}
/*
* Check the database hashes of all databases to ensure each node of the replica set has the same
* data.
*/
function assertSameData(primary, conns) {
var dbs = primary.getDBs().databases;
for (var i in dbs) {
var db1 = primary.getDB(dbs[i].name);
for (var j in conns) {
var conn = conns[j];
if (!isArbiter(conn)) {
var db2 = conn.getDB(dbs[i].name);
assertDBsEq(db1, db2);
}
}
}
}
/*
* function to pass to a thread to make it start doing random commands/CRUD operations.
*/
function startCmds(randomOps, host) {
var ops = [
"insert", "remove", "update", "renameCollection", "dropDatabase",
"dropCollection", "createIndex", "dropIndex", "collMod", "emptyCapped", "applyOps",
"createCollection", "convertToCapped", "appendOplogNote"
];
var m = new Mongo(host);
var numOps = 200;
randomOps.doRandomWork(m, numOps, ops);
return true;
}
/*
* function to pass to a thread to make it start doing random CRUD operations.
*/
function startCRUD(randomOps, host) {
var m = new Mongo(host);
var numOps = 500;
randomOps.doRandomWork(m, numOps, ["insert", "update", "remove"]);
return true;
}
/*
* To avoid race conditions on things like trying to drop a collection while another thread is
* trying to rename it, just have one thread that might issue commands, and the others do random
* CRUD operations. To be clear, this is something that the Mongod should be able to handle, but
* this test code does not have atomic random operations. E.g. it has to first randomly select
* a collection to drop an index from, which may not be there by the time it tries to get a list
* of indices on the collection.
*/
function doMultiThreadedWork(primary, numThreads) {
var threads = [];
// The command thread
// Note we pass the hostname, as we have to re-establish the connection in the new thread.
var cmdThread = new ScopedThread(startCmds, RandomOps, primary.host);
threads.push(cmdThread);
cmdThread.start();
// Other CRUD threads
for (var i = 1; i < numThreads; i++) {
var crudThread = new ScopedThread(startCRUD, RandomOps, primary.host);
threads.push(crudThread);
crudThread.start();
}
for (var j = 0; j < numThreads; j++) {
assert.eq(threads[j].returnData(), true);
}
}
//////////////////////////////////////////////////////////////////////////////////
// START ACTUAL TESTING
//////////////////////////////////////////////////////////////////////////////////
(function() {
"use strict";
var name = "mixed_storage_and_version";
// Create a replica set with 2 nodes of each of the types below, plus one arbiter.
var oldVersion = "last-stable";
var newVersion = "latest";
var setups = [{binVersion: newVersion, storageEngine: 'mmapv1'},
{binVersion: newVersion, storageEngine: 'wiredTiger'},
{binVersion: oldVersion}
];
var nodes = {};
var node = 0;
// Add each one twice.
for (var i in setups) {
nodes["n" + node] = setups[i];
node++;
nodes["n" + node] = setups[i];
node++;
}
nodes["n" + 2 * setups.length] = {arbiter: true};
var replTest = new ReplSetTest({nodes: nodes, name: name});
var conns = replTest.startSet();
var config = replTest.getReplSetConfig();
// Make sure everyone is syncing from the primary, to ensure we have all combinations of
// primary/secondary syncing.
config.settings = {chainingAllowed: false};
replTest.initiate();
// Ensure all are synced.
replTest.awaitSecondaryNodes(120000);
var primary = replTest.getPrimary();
// Keep track of the indices of different types of primaries.
// We'll rotate to get a primary of each type.
var possiblePrimaries = [0,2,4];
var highestPriority = 2;
while (possiblePrimaries.length > 0) {
config = primary.getDB("local").system.replset.findOne();
var primaryIndex = RandomOps.randomChoice(possiblePrimaries);
print("TRANSITIONING to " + tojsononeline(setups[primaryIndex/2]) + " as primary");
// Remove chosen type from future choices.
removeFromArray(primaryIndex, possiblePrimaries);
config.members[primaryIndex].priority = highestPriority;
if (config.version === undefined) {
config.version = 2;
}
else {
config.version++;
}
highestPriority++;
printjson(config);
try {
primary.getDB("admin").runCommand({replSetReconfig: config});
}
catch(e) {
// Expected to fail, as we'll have to reconnect.
}
replTest.awaitReplication(60000); // 2 times the election period.
assert.soon(primaryChanged(conns, replTest, primaryIndex),
"waiting for higher priority primary to be elected", 100000);
print("New primary elected, doing a bunch of work");
primary = replTest.getPrimary();
doMultiThreadedWork(primary, 10);
replTest.awaitReplication(50000);
print("Work done, checking to see all nodes match");
assertSameData(primary, conns);
}
})();