diff options
author | Charlie Swanson <cswanson310@gmail.com> | 2014-12-11 14:04:34 -0500 |
---|---|---|
committer | Matt Kangas <matt.kangas@mongodb.com> | 2014-12-11 16:48:44 -0500 |
commit | 8b3024fad3b474d0caf9a8aecb0f464f8138b564 (patch) | |
tree | fce5a6178b333f7b25931b062feeefaba77ae778 | |
parent | c5cfff053584d58fe75d6e0bfbcb33bb6f46dbd8 (diff) | |
download | mongo-8b3024fad3b474d0caf9a8aecb0f464f8138b564.tar.gz |
SERVER-16508 Add tests for mixed version/storage engine replica sets.
Closes #879
Signed-off-by: Matt Kangas <matt.kangas@mongodb.com>
-rw-r--r-- | jstests/multiVersion/initialize_from_old_node.js | 22 | ||||
-rw-r--r-- | jstests/multiVersion/mixed_storage_version_replication.js | 579 |
2 files changed, 601 insertions, 0 deletions
diff --git a/jstests/multiVersion/initialize_from_old_node.js b/jstests/multiVersion/initialize_from_old_node.js new file mode 100644 index 00000000000..f9d9e9bf1ab --- /dev/null +++ b/jstests/multiVersion/initialize_from_old_node.js @@ -0,0 +1,22 @@ +/* + * This is a regression test for SERVER-16189, to make sure a replica set with both 2.8 and 2.6 + * nodes can be initialized from a 2.6 node. + */ + +(function() { + "use strict"; + var name = "initialize_from_old"; + var oldVersion = '2.6'; + var newVersion = 'latest'; + var nodes = { + n0: {binVersion: oldVersion}, + n1: {binVersion: newVersion}, + n2: {binVersion: newVersion} + }; + var rst = new ReplSetTest({nodes: nodes, name: name}); + var conns = rst.startSet(); + var oldNode = conns[0]; + var config = rst.getReplSetConfig(); + var response = oldNode.getDB("admin").runCommand({replSetInitiate: config}); + assert.eq(response.ok, 1); +})(); diff --git a/jstests/multiVersion/mixed_storage_version_replication.js b/jstests/multiVersion/mixed_storage_version_replication.js new file mode 100644 index 00000000000..980aa88018e --- /dev/null +++ b/jstests/multiVersion/mixed_storage_version_replication.js @@ -0,0 +1,579 @@ +/* + * 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) { + var randIndex = Random.randInt(0, collection.find().count()); + return collection.find().sort({$natural: 1}).skip(randIndex).limit(1)[0]; + }, + + /* + * 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. + } + // If multithreaded, doc might be undefined. + var doc = this.getRandomDoc(coll); + if (this.verbose) { + print("Deleting:"); + printjson(doc); + } + // If multithreaded, doc might not exist anymore. + 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. + } + // If multithreaded, doc might be undefined. + var doc = this.getRandomDoc(coll); + 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(); + }; +}; + +/* + * 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 db = dbs[i]; + // Resulting document has the following form: + // {md5: <hash of all>, collections: {collectionName: <hash of that collection}, ...} + var primaryHash = primary.getDB(db.name).runCommand({dbHash: 1}); + // Make sure the hash is the same on all nodes. + for (var j in conns) { + var conn = conns[j]; + if (!isArbiter(conn)) { + var secondaryHash = conn.getDB(db.name).runCommand({dbHash: 1}); + if (db.name === 'local') { + // We don't expect the entire local collection to be the same, just the oplog. + assert.eq(secondaryHash.collections["oplog.rs"], + primaryHash.collections["oplog.rs"], + "oplog differs on " + primary.host + " and " + conn.host); + } + else { + assert.eq( + secondaryHash.md5, primaryHash.md5, + "Database " + db.name + " differs on " + primary.host + " and " + conn.host + ); + } + } + } + } +} + +/* + * 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 = "2.6"; + 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(); + 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); + } +})(); |