summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/replsets/apply_ops_concurrent_non_atomic.js154
1 files changed, 154 insertions, 0 deletions
diff --git a/jstests/replsets/apply_ops_concurrent_non_atomic.js b/jstests/replsets/apply_ops_concurrent_non_atomic.js
new file mode 100644
index 00000000000..3650178e30f
--- /dev/null
+++ b/jstests/replsets/apply_ops_concurrent_non_atomic.js
@@ -0,0 +1,154 @@
+/**
+ * This test ensures that multiple non-atomic applyOps commands can run concurrently.
+ * Prior to SERVER-29802, applyOps would acquire the global lock regardless of the
+ * atomicity of the operations (as a whole) being applied.
+ */
+(function() {
+ 'use strict';
+
+ /**
+ * Creates an array of insert operations for applyOps into collection 'coll'.
+ */
+ function generateInsertOps(coll, numOps) {
+ // Explicit 'use strict' to prevent mozjs from injecting its own "use strict" directive
+ // (with incorrect indentation) when we convert this function into a string for
+ // startParallelShell().
+ 'use strict';
+ const ops = Array(numOps).fill('ignored').map((unused, i) => {
+ return {op: 'i', ns: coll.getFullName(), o: {_id: i}};
+ });
+ return ops;
+ }
+
+ /**
+ * Runs applyOps in non-atomic mode to insert 'numOps' documents into collection 'coll'.
+ */
+ function applyOpsInsertNonAtomic(coll, numOps) {
+ 'use strict';
+ const ops = generateInsertOps(coll, numOps);
+ const mydb = coll.getDB();
+ assert.commandWorked(mydb.runCommand({applyOps: ops, allowAtomic: false}),
+ 'failed to insert documents into ' + coll.getFullName());
+ }
+
+ /**
+ * Creates a function for startParallelShell() to run that will insert documents into collection
+ * 'coll' using applyOps.
+ */
+ function createInsertFunction(coll, numOps) {
+ const options = {
+ dbName: coll.getDB().getName(),
+ collName: coll.getName(),
+ numOps: numOps,
+ };
+ const functionName = 'insertFunction_' + coll.getFullName().replace(/\./g, '_');
+ const insertFunction = function(options) {
+ 'use strict';
+
+ const mydb = db.getSiblingDB(options.dbName);
+ const coll = mydb.getCollection(options.collName);
+ const numOps = options.numOps;
+
+ jsTestLog('Starting to apply ' + numOps + ' operations in collection ' +
+ coll.getFullName());
+ applyOpsInsertNonAtomic(coll, numOps);
+ jsTestLog('Successfully applied ' + numOps + ' operations in collection ' +
+ coll.getFullName());
+ };
+ const s = //
+ '\n\n' + //
+ 'const generateInsertOps = ' + generateInsertOps + ';\n\n' + //
+ 'const applyOpsInsertNonAtomic = ' + applyOpsInsertNonAtomic + ';\n\n' + //
+ 'const ' + functionName + ' = ' + insertFunction + ';\n\n' + //
+ functionName + '(' + tojson(options) + ');'; //
+ return s;
+ }
+
+ /**
+ * Returns number of insert operations reported by serverStatus.
+ * Depending on the server version, applyOps may increment either 'opcounters' or
+ * 'opcountersRepl':
+ * since 3.6: 'opcounters.insert'
+ * 3.4 and older: 'opcountersRepl.insert'
+ */
+ function getInsertOpCount(serverStatus) {
+ return (serverStatus.version.substr(0, 3) === "3.4") ? serverStatus.opcountersRepl.insert
+ : serverStatus.opcounters.insert;
+ }
+
+ const replTest = new ReplSetTest({nodes: 1});
+ replTest.startSet();
+ replTest.initiate();
+
+ const primary = replTest.getPrimary();
+ const adminDb = primary.getDB('admin');
+ const db1 = primary.getDB('test1');
+ const coll1 = db1.coll1;
+ const db2 = primary.getDB('test2');
+ const coll2 = db2.coll2;
+
+ assert.commandWorked(db1.createCollection(coll1.getName()));
+ assert.commandWorked(db2.createCollection(coll2.getName()));
+
+ // Enable fail point to pause applyOps between operations.
+ assert.commandWorked(primary.adminCommand(
+ {configureFailPoint: 'applyOpsPauseBetweenOperations', mode: 'alwaysOn'}));
+
+ // This logs each operation being applied.
+ const previousLogLevel =
+ assert.commandWorked(primary.setLogLevel(3, 'replication')).was.replication.verbosity;
+
+ jsTestLog('Applying operations in collections ' + coll1.getFullName() + ' and ' +
+ coll2.getFullName());
+
+ const numOps = 100;
+ const insertProcess1 =
+ startParallelShell(createInsertFunction(coll1, numOps), replTest.getPort(0));
+ const insertProcess2 =
+ startParallelShell(createInsertFunction(coll2, numOps), replTest.getPort(0));
+
+ // The fail point will prevent applyOps from advancing past the first operation in each batch of
+ // operations. If applyOps is applying both sets of operations concurrently without holding the
+ // global lock, the insert opcounter will eventually be incremented to 2.
+ try {
+ let insertOpCount = 0;
+ const expectedFinalOpCount = 1;
+ assert.soon(
+ function() {
+ const serverStatus = adminDb.serverStatus();
+ insertOpCount = getInsertOpCount(serverStatus);
+ // This assertion may fail if the fail point is not implemented correctly within
+ // applyOps. This allows us to fail fast instead of waiting for the assert.soon()
+ // function to time out.
+ assert.lte(insertOpCount,
+ expectedFinalOpCount,
+ 'Expected at most ' + expectedFinalOpCount +
+ ' documents inserted with fail point enabled. ' +
+ 'Most recent insert operation count = ' + insertOpCount);
+ return insertOpCount === expectedFinalOpCount;
+ },
+ 'Insert operation count did not reach ' + expectedFinalOpCount +
+ ' as expected with fail point enabled. Most recent insert operation count = ' +
+ insertOpCount);
+ } finally {
+ assert.commandWorked(primary.adminCommand(
+ {configureFailPoint: 'applyOpsPauseBetweenOperations', mode: 'off'}));
+ }
+
+ insertProcess1();
+ insertProcess2();
+
+ jsTestLog('Successfully applied operations in collections ' + coll1.getFullName() + ' and ' +
+ coll2.getFullName());
+
+ // Reset log level.
+ primary.setLogLevel(previousLogLevel, 'replication');
+
+ const serverStatus = adminDb.serverStatus();
+ assert.eq(200,
+ getInsertOpCount(serverStatus),
+ 'incorrect number of insert operations in server status after applyOps: ' +
+ tojson(serverStatus));
+
+ replTest.stopSet();
+})();