diff options
author | Matthew Russotto <matthew.russotto@10gen.com> | 2018-02-20 11:56:59 -0500 |
---|---|---|
committer | Matthew Russotto <matthew.russotto@10gen.com> | 2018-03-01 08:27:54 -0500 |
commit | ae6827297917916173c4d522cabce8fe8fecbf27 (patch) | |
tree | 2b8a9b55dd35b70f010a03b90288a1c95d722760 /jstests | |
parent | c713377e4b99475563f26de19f88c45ece8a0546 (diff) | |
download | mongo-ae6827297917916173c4d522cabce8fe8fecbf27.tar.gz |
SERVER-32320 Push doTxn oplog entry generation into logOp on transaction commit.
Diffstat (limited to 'jstests')
-rw-r--r-- | jstests/auth/lib/commands_lib.js | 120 | ||||
-rw-r--r-- | jstests/core/bypass_doc_validation.js | 14 | ||||
-rw-r--r-- | jstests/core/collation.js | 32 | ||||
-rw-r--r-- | jstests/core/do_txn_basic.js | 189 | ||||
-rw-r--r-- | jstests/core/do_txn_oneshot.js | 80 | ||||
-rw-r--r-- | jstests/core/do_txn_oplog_entry.js (renamed from jstests/core/do_txn_atomicity.js) | 34 | ||||
-rw-r--r-- | jstests/core/json_schema/misc_validation.js | 11 | ||||
-rw-r--r-- | jstests/core/views/views_all_commands.js | 13 |
8 files changed, 375 insertions, 118 deletions
diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 588fd06a6fb..d185de225e6 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -57,25 +57,28 @@ to be authorized to run the command. 4) skipSharded -Add "skipSharded: true" if you want to run the test only on a standalone. +Add "skipSharded: true" if you want to run the test only ony in a non-sharded configuration. -5) skipStandalone +5) skipUnlessSharded -Add "skipStandalone: true" if you want to run the test only in sharded +Add "skipUnlessSharded: true" if you want to run the test only in sharded configuration. -6) setup +6) skipUnlessReplicaSet +Add "skipUnlessReplicaSet: true" if you want to run the test only when replica sets are in use. + +7) setup The setup function, if present, is called before testing whether a particular role authorizes a command for a particular database. -7) teardown +8) teardown The teardown function, if present, is called immediately after testint whether a particular role authorizes a command for a particular database. -8) privileges +9) privileges An array of privileges used when testing user-defined roles. The test case tests that a user with the specified privileges is authorized to run the command, and that having only a subset of the @@ -178,6 +181,8 @@ var roles_all = { }; load("jstests/libs/uuid_util.js"); +// For isReplSet +load("jstests/libs/fixture_helpers.js"); var authCommandsLib = { @@ -198,7 +203,7 @@ var authCommandsLib = { { testname: "addShard", command: {addShard: "x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -221,7 +226,7 @@ var authCommandsLib = { u: {_id: {ns: "test.x", min: 1}, ns: "test.x"} }] }, - skipStandalone: true, + skipUnlessSharded: true, testcases: [{ runOnDb: "config", roles: roles_clusterManager, @@ -2264,7 +2269,7 @@ var authCommandsLib = { { testname: "balancerStart", command: {balancerStart: 1}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -2285,7 +2290,7 @@ var authCommandsLib = { { testname: "balancerStop", command: {balancerStop: 1}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -2306,7 +2311,7 @@ var authCommandsLib = { { testname: "balancerStatus", command: {balancerStatus: 1}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -2917,9 +2922,12 @@ var authCommandsLib = { "ns": firstDbName + ".x", "o": {"_id": ObjectId("57dc3d7da4fce4358afa85b8"), "data": 5} }], - preCondition: [{ns: firstDbName + ".x", q: {x: 5}, res: []}] + preCondition: [{ns: firstDbName + ".x", q: {x: 5}, res: []}], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { db.getSisterDB(firstDbName).x.save({}); }, @@ -2943,9 +2951,12 @@ var authCommandsLib = { "op": "i", "ns": firstDbName + ".x", "o": {"_id": ObjectId("57dc3d7da4fce4358afa85b8"), "data": 5} - }] + }], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { db.getSisterDB(firstDbName).x.save({}); }, @@ -2971,10 +2982,13 @@ var authCommandsLib = { "ns": state.collName, "ui": state.uuid, "o": {"_id": ObjectId("57dc3d7da4fce4358afa85b8"), "data": 5} - }] + }], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }; }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { var sibling = db.getSisterDB(firstDbName); sibling.runCommand({create: "x"}); @@ -3008,10 +3022,13 @@ var authCommandsLib = { // Given a nonexistent UUID. The command should fail. "ui": UUID("71f1d1d7-68ca-493e-a7e9-f03c94e2e960"), "o": {"_id": ObjectId("57dc3d7da4fce4358afa85b8"), "data": 5} - }] + }], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }; }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { var sibling = db.getSisterDB(firstDbName); sibling.runCommand({create: "x"}); @@ -3047,10 +3064,13 @@ var authCommandsLib = { "ns": state.collName, "ui": state.uuid, "o": {"_id": ObjectId("57dc3d7da4fce4358afa85b8"), "data": 5} - }] + }], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }; }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { var sibling = db.getSisterDB(firstDbName); sibling.runCommand({create: "x"}); @@ -3084,10 +3104,13 @@ var authCommandsLib = { firstDbName + ".y", // Specify wrong name but correct uuid. Should work. "ui": state.x_uuid, // The insert should on x "o": {"_id": ObjectId("57dc3d7da4fce4358afa85b8"), "data": 5} - }] + }], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }; }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { db.getSisterDB(firstDbName).x.drop(); db.getSisterDB(firstDbName).y.drop(); @@ -3123,10 +3146,13 @@ var authCommandsLib = { firstDbName + ".y", // Specify wrong name but correct uuid. Should work. "ui": state.x_uuid, // The insert should on x "o": {"_id": ObjectId("57dc3d7da4fce4358afa85b8"), "data": 5} - }] + }], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }; }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { db.getSisterDB(firstDbName).x.drop(); db.getSisterDB(firstDbName).y.drop(); @@ -3161,9 +3187,12 @@ var authCommandsLib = { "ns": firstDbName + ".x", "o2": {"_id": 1}, "o": {"_id": 1, "data": 8} - }] + }], + txnNumber: NumberLong(0), + lsid: {id: UUID()} }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { db.getSisterDB(firstDbName).x.save({_id: 1, data: 1}); }, @@ -3189,9 +3218,11 @@ var authCommandsLib = { "o2": {"_id": 1}, "o": {"_id": 1, "data": 8} }], - alwaysUpsert: false + txnNumber: NumberLong(0), + lsid: {id: UUID()} }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { db.getSisterDB(firstDbName).x.save({_id: 1, data: 1}); }, @@ -3219,10 +3250,12 @@ var authCommandsLib = { "o2": {"_id": 1}, "o": {"_id": 1, "data": 8} }], - alwaysUpsert: false + txnNumber: NumberLong(0), + lsid: {id: UUID()} }; }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { var sibling = db.getSisterDB(firstDbName); sibling.x.save({_id: 1, data: 1}); @@ -3257,10 +3290,12 @@ var authCommandsLib = { "o2": {"_id": 1}, "o": {"_id": 1, "data": 8} }], - alwaysUpsert: false + txnNumber: NumberLong(0), + lsid: {id: UUID()} }; }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { var sibling = db.getSisterDB(firstDbName); sibling.x.save({_id: 1, data: 1}); @@ -3285,8 +3320,13 @@ var authCommandsLib = { }, { testname: "doTxn_delete", - command: {doTxn: [{"op": "d", "ns": firstDbName + ".x", "o": {"_id": 1}}]}, + command: { + doTxn: [{"op": "d", "ns": firstDbName + ".x", "o": {"_id": 1}}], + txnNumber: NumberLong(0), + lsid: {id: UUID()} + }, skipSharded: true, + skipUnlessReplicaSet: true, setup: function(db) { db.getSisterDB(firstDbName).x.save({_id: 1, data: 1}); }, @@ -3403,7 +3443,7 @@ var authCommandsLib = { { testname: "enableSharding", command: {enableSharding: "x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -3742,7 +3782,7 @@ var authCommandsLib = { { testname: "flushRouterConfig", command: {flushRouterConfig: 1}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -4269,7 +4309,7 @@ var authCommandsLib = { { testname: "killOp", // sharded version command: {killOp: 1, op: "shard1:123"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -4437,7 +4477,7 @@ var authCommandsLib = { { testname: "listShards", command: {listShards: 1}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -4540,7 +4580,7 @@ var authCommandsLib = { { testname: "s_mergeChunks", command: {mergeChunks: "test.x", bounds: [{i: 0}, {i: 5}]}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -4570,7 +4610,7 @@ var authCommandsLib = { { testname: "s_moveChunk", command: {moveChunk: "test.x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -4600,7 +4640,7 @@ var authCommandsLib = { { testname: "movePrimary", command: {movePrimary: "x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -4615,7 +4655,7 @@ var authCommandsLib = { { testname: "netstat", command: {netstat: "x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -4903,7 +4943,7 @@ var authCommandsLib = { { testname: "removeShard", command: {removeShard: "x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -5239,7 +5279,7 @@ var authCommandsLib = { { testname: "shardCollection", command: {shardCollection: "test.x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -5274,7 +5314,7 @@ var authCommandsLib = { { testname: "split", command: {split: "test.x"}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -5531,7 +5571,7 @@ var authCommandsLib = { { testname: "addShardToZone", command: {addShardToZone: shard0name, zone: 'z'}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -5551,7 +5591,7 @@ var authCommandsLib = { { testname: "removeShardFromZone", command: {removeShardFromZone: shard0name, zone: 'z'}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -5574,7 +5614,7 @@ var authCommandsLib = { { testname: "updateZoneKeyRange", command: {updateZoneKeyRange: 'test.foo', min: {x: 1}, max: {x: 5}, zone: 'z'}, - skipStandalone: true, + skipUnlessSharded: true, testcases: [ { runOnDb: adminDbName, @@ -5665,7 +5705,11 @@ var authCommandsLib = { return []; } // others shouldn't run in a standalone environment - if (t.skipStandalone && !this.isMongos(conn)) { + if (t.skipUnlessSharded && !this.isMongos(conn)) { + return []; + } + // some tests require replica sets to be enabled. + if (t.skipUnlessReplicaSet && !FixtureHelpers.isReplSet(conn.getDB("admin"))) { return []; } diff --git a/jstests/core/bypass_doc_validation.js b/jstests/core/bypass_doc_validation.js index 107c4f2aba4..bed22676dc4 100644 --- a/jstests/core/bypass_doc_validation.js +++ b/jstests/core/bypass_doc_validation.js @@ -17,6 +17,8 @@ // For isMMAPv1. load("jstests/concurrency/fsm_workload_helpers/server_types.js"); + // For isReplSet + load("jstests/libs/fixture_helpers.js"); function assertFailsValidation(res) { if (res instanceof WriteResult || res instanceof BulkWriteResult) { @@ -53,12 +55,16 @@ assert.eq(1, coll.count({_id: 9})); } - // Test doTxn with a simple insert if not on mongos and not on MMAPv1. - if (!isMongos && !isMMAPv1(db)) { + // Test doTxn with a simple insert if a replica set, not on mongos and not on MMAPv1. + if (FixtureHelpers.isReplSet(db) && !isMongos && !isMMAPv1(db)) { + const session = db.getMongo().startSession(); + const sessionDb = session.getDatabase(myDb.getName()); const op = [{op: 'i', ns: coll.getFullName(), o: {_id: 10}}]; - assertFailsValidation(myDb.runCommand({doTxn: op, bypassDocumentValidation: false})); + assertFailsValidation(sessionDb.runCommand( + {doTxn: op, bypassDocumentValidation: false, txnNumber: NumberLong("0")})); assert.eq(0, coll.count({_id: 10})); - assert.commandWorked(myDb.runCommand({doTxn: op, bypassDocumentValidation: true})); + assert.commandWorked(sessionDb.runCommand( + {doTxn: op, bypassDocumentValidation: true, txnNumber: NumberLong("1")})); assert.eq(1, coll.count({_id: 10})); } diff --git a/jstests/core/collation.js b/jstests/core/collation.js index 3a089dde4ee..2b3996ccc96 100644 --- a/jstests/core/collation.js +++ b/jstests/core/collation.js @@ -11,6 +11,8 @@ load("jstests/libs/get_index_helpers.js"); // For isMMAPv1. load("jstests/concurrency/fsm_workload_helpers/server_types.js"); + // For isReplSet + load("jstests/libs/fixture_helpers.js"); var coll = db.collation; coll.drop(); @@ -1973,39 +1975,47 @@ } // doTxn - if (!isMongos && !isMMAPv1(db)) { + if (FixtureHelpers.isReplSet(db) && !isMongos && !isMMAPv1(db)) { + const session = db.getMongo().startSession(); + const sessionDb = session.getDatabase(db.getName()); coll.drop(); assert.commandWorked( db.createCollection("collation", {collation: {locale: "en_US", strength: 2}})); assert.writeOK(coll.insert({_id: "foo", x: 5, str: "bar"})); // preCondition.q respects collection default collation. - assert.commandFailed(db.runCommand({ + assert.commandFailed(sessionDb.runCommand({ doTxn: [{op: "u", ns: coll.getFullName(), o2: {_id: "foo"}, o: {$set: {x: 6}}}], - preCondition: [{ns: coll.getFullName(), q: {_id: "not foo"}, res: {str: "bar"}}] + preCondition: [{ns: coll.getFullName(), q: {_id: "not foo"}, res: {str: "bar"}}], + txnNumber: NumberLong("0") })); assert.eq(5, coll.findOne({_id: "foo"}).x); - assert.commandWorked(db.runCommand({ + assert.commandWorked(sessionDb.runCommand({ doTxn: [{op: "u", ns: coll.getFullName(), o2: {_id: "foo"}, o: {$set: {x: 6}}}], - preCondition: [{ns: coll.getFullName(), q: {_id: "FOO"}, res: {str: "bar"}}] + preCondition: [{ns: coll.getFullName(), q: {_id: "FOO"}, res: {str: "bar"}}], + txnNumber: NumberLong("1") })); assert.eq(6, coll.findOne({_id: "foo"}).x); // preCondition.res respects collection default collation. - assert.commandFailed(db.runCommand({ + assert.commandFailed(sessionDb.runCommand({ doTxn: [{op: "u", ns: coll.getFullName(), o2: {_id: "foo"}, o: {$set: {x: 7}}}], - preCondition: [{ns: coll.getFullName(), q: {_id: "foo"}, res: {str: "not bar"}}] + preCondition: [{ns: coll.getFullName(), q: {_id: "foo"}, res: {str: "not bar"}}], + txnNumber: NumberLong("2") })); assert.eq(6, coll.findOne({_id: "foo"}).x); - assert.commandWorked(db.runCommand({ + assert.commandWorked(sessionDb.runCommand({ doTxn: [{op: "u", ns: coll.getFullName(), o2: {_id: "foo"}, o: {$set: {x: 7}}}], - preCondition: [{ns: coll.getFullName(), q: {_id: "foo"}, res: {str: "BAR"}}] + preCondition: [{ns: coll.getFullName(), q: {_id: "foo"}, res: {str: "BAR"}}], + txnNumber: NumberLong("3") })); assert.eq(7, coll.findOne({_id: "foo"}).x); // <operation>.o2 respects collection default collation. - assert.commandWorked(db.runCommand( - {doTxn: [{op: "u", ns: coll.getFullName(), o2: {_id: "FOO"}, o: {$set: {x: 8}}}]})); + assert.commandWorked(sessionDb.runCommand({ + doTxn: [{op: "u", ns: coll.getFullName(), o2: {_id: "FOO"}, o: {$set: {x: 8}}}], + txnNumber: NumberLong("4") + })); assert.eq(8, coll.findOne({_id: "foo"}).x); } diff --git a/jstests/core/do_txn_basic.js b/jstests/core/do_txn_basic.js index 8d12ec377d0..199b4948c8e 100644 --- a/jstests/core/do_txn_basic.js +++ b/jstests/core/do_txn_basic.js @@ -6,6 +6,8 @@ load("jstests/libs/get_index_helpers.js"); // For isMMAPv1. load("jstests/concurrency/fsm_workload_helpers/server_types.js"); + // For isReplSet + load("jstests/libs/fixture_helpers.js"); const t = db.do_txn1; @@ -18,6 +20,14 @@ jsTestLog("Skipping test as the storage engine does not support doTxn."); return; } + if (!FixtureHelpers.isReplSet(db)) { + jsTestLog("Skipping test as doTxn requires a replSet and replication is not enabled."); + return; + } + + var session = db.getMongo().startSession(); + db = session.getDatabase("test"); + var txnNumber = 0; t.drop(); @@ -26,56 +36,71 @@ // // Empty array of operations. - assert.commandFailedWithCode(db.adminCommand({doTxn: []}), + assert.commandFailedWithCode(db.adminCommand({doTxn: [], txnNumber: NumberLong(txnNumber++)}), ErrorCodes.InvalidOptions, 'doTxn should fail on empty array of operations'); // Non-array type for operations. - assert.commandFailedWithCode(db.adminCommand({doTxn: "not an array"}), - ErrorCodes.TypeMismatch, - 'doTxn should fail on non-array type for operations'); + assert.commandFailedWithCode( + db.adminCommand({doTxn: "not an array", txnNumber: NumberLong(txnNumber++)}), + ErrorCodes.TypeMismatch, + 'doTxn should fail on non-array type for operations'); // Missing 'op' field in an operation. - assert.commandFailedWithCode(db.adminCommand({doTxn: [{ns: t.getFullName(), o: {_id: 0}}]}), - ErrorCodes.FailedToParse, - 'doTxn should fail on operation without "op" field'); - - // Non-string 'op' field in an operation. assert.commandFailedWithCode( - db.adminCommand({doTxn: [{op: 12345, ns: t.getFullName(), o: {_id: 0}}]}), + db.adminCommand( + {doTxn: [{ns: t.getFullName(), o: {_id: 0}}], txnNumber: NumberLong(txnNumber++)}), ErrorCodes.FailedToParse, - 'doTxn should fail on operation with non-string "op" field'); + 'doTxn should fail on operation without "op" field'); + + // Non-string 'op' field in an operation. + assert.commandFailedWithCode(db.adminCommand({ + doTxn: [{op: 12345, ns: t.getFullName(), o: {_id: 0}}], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.FailedToParse, + 'doTxn should fail on operation with non-string "op" field'); // Empty 'op' field value in an operation. - assert.commandFailedWithCode( - db.adminCommand({doTxn: [{op: '', ns: t.getFullName(), o: {_id: 0}}]}), - ErrorCodes.FailedToParse, - 'doTxn should fail on operation with empty "op" field value'); + assert.commandFailedWithCode(db.adminCommand({ + doTxn: [{op: '', ns: t.getFullName(), o: {_id: 0}}], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.FailedToParse, + 'doTxn should fail on operation with empty "op" field value'); // Missing 'ns' field in an operation. - assert.commandFailedWithCode(db.adminCommand({doTxn: [{op: 'u', o: {_id: 0}}]}), - ErrorCodes.FailedToParse, - 'doTxn should fail on operation without "ns" field'); + assert.commandFailedWithCode( + db.adminCommand({doTxn: [{op: 'u', o: {_id: 0}}], txnNumber: NumberLong(txnNumber++)}), + ErrorCodes.FailedToParse, + 'doTxn should fail on operation without "ns" field'); // Missing 'o' field in an operation. - assert.commandFailedWithCode(db.adminCommand({doTxn: [{op: 'u', ns: t.getFullName()}]}), - ErrorCodes.FailedToParse, - 'doTxn should fail on operation without "o" field'); + assert.commandFailedWithCode( + db.adminCommand( + {doTxn: [{op: 'u', ns: t.getFullName()}], txnNumber: NumberLong(txnNumber++)}), + ErrorCodes.FailedToParse, + 'doTxn should fail on operation without "o" field'); // Non-string 'ns' field in an operation. - assert.commandFailedWithCode(db.adminCommand({doTxn: [{op: 'u', ns: 12345, o: {_id: 0}}]}), - ErrorCodes.FailedToParse, - 'doTxn should fail on operation with non-string "ns" field'); + assert.commandFailedWithCode( + db.adminCommand( + {doTxn: [{op: 'u', ns: 12345, o: {_id: 0}}], txnNumber: NumberLong(txnNumber++)}), + ErrorCodes.FailedToParse, + 'doTxn should fail on operation with non-string "ns" field'); // Missing dbname in 'ns' field. assert.commandFailedWithCode( - db.adminCommand({doTxn: [{op: 'd', ns: t.getName(), o: {_id: 1}}]}), + db.adminCommand( + {doTxn: [{op: 'd', ns: t.getName(), o: {_id: 1}}], txnNumber: NumberLong(txnNumber++)}), ErrorCodes.InvalidNamespace, 'doTxn should fail with a missing dbname in the "ns" field value'); // Empty 'ns' field value. - assert.commandFailed(db.adminCommand({doTxn: [{op: 'u', ns: '', o: {_id: 0}}]}), - 'doTxn should fail with empty "ns" field value'); + assert.commandFailed( + db.adminCommand( + {doTxn: [{op: 'u', ns: '', o: {_id: 0}}], txnNumber: NumberLong(txnNumber++)}), + 'doTxn should fail with empty "ns" field value'); // Valid 'ns' field value in unknown operation type 'x'. assert.commandFailedWithCode( @@ -84,17 +109,69 @@ 'doTxn should fail on unknown operation type "x" with valid "ns" value'); // Illegal operation type 'n' (no-op). - assert.commandFailedWithCode( - db.adminCommand({doTxn: [{op: 'n', ns: t.getFullName(), o: {_id: 0}}]}), - ErrorCodes.InvalidOptions, - 'doTxn should fail on "no op" operations.'); + assert.commandFailedWithCode(db.adminCommand({ + doTxn: [{op: 'n', ns: t.getFullName(), o: {_id: 0}}], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.InvalidOptions, + 'doTxn should fail on "no op" operations.'); // Illegal operation type 'c' (command). + assert.commandFailedWithCode(db.adminCommand({ + doTxn: [{op: 'c', ns: t.getCollection('$cmd').getFullName(), o: {applyOps: []}}], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.InvalidOptions, + 'doTxn should fail on commands.'); + + // No transaction number in an otherwise valid operation. assert.commandFailedWithCode( - db.adminCommand( - {doTxn: [{op: 'c', ns: t.getCollection('$cmd').getFullName(), o: {applyOps: []}}]}), + db.adminCommand({doTxn: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]}), ErrorCodes.InvalidOptions, - 'doTxn should fail on commands.'); + 'doTxn should fail when no transaction number is given.'); + + // Session IDs and transaction numbers on sub-ops are not allowed + var lsid = {id: UUID()}; + res = assert.commandFailedWithCode( + db.runCommand({ + doTxn: [{ + op: "i", + ns: t.getFullName(), + o: {_id: 7, x: 24}, + lsid: lsid, + txnNumber: NumberLong(1), + }], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.FailedToParse, + 'doTxn should fail when inner transaction contains session id.'); + + res = assert.commandFailedWithCode( + db.runCommand({ + doTxn: [{ + op: "u", + ns: t.getFullName(), + o2: {_id: 7}, + o: {$set: {x: 25}}, + txnNumber: NumberLong(1), + }], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.FailedToParse, + 'doTxn should fail when inner transaction contains transaction number.'); + + res = assert.commandFailedWithCode( + db.runCommand({ + doTxn: [{ + op: "d", + ns: t.getFullName(), + o: {_id: 7}, + stmtId: 0, + }], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.FailedToParse, + 'doTxn should fail when inner transaction contains statement id.'); // Malformed operation with unexpected field 'x'. assert.commandFailedWithCode( @@ -115,7 +192,7 @@ const t2 = db.getSiblingDB('do_txn1_no_such_db').getCollection('t'); [t, t2].forEach(coll => { const op = {op: optype, ns: coll.getFullName(), o: o, o2: o2}; - const cmd = {doTxn: [op]}; + const cmd = {doTxn: [op], txnNumber: NumberLong(txnNumber++)}; jsTestLog('Testing doTxn on non-existent namespace: ' + tojson(cmd)); if (expectedErrorCode === ErrorCodes.OK) { assert.commandWorked(db.adminCommand(cmd)); @@ -132,13 +209,17 @@ testCrudOperationOnNonExistentNamespace('u', {x: 0}, {_id: 0}, ErrorCodes.NamespaceNotFound); assert.commandWorked(db.createCollection(t.getName())); - var a = assert.commandWorked( - db.adminCommand({doTxn: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]})); + var a = assert.commandWorked(db.adminCommand({ + doTxn: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}], + txnNumber: NumberLong(txnNumber++) + })); assert.eq(1, t.find().count(), "Valid insert failed"); assert.eq(true, a.results[0], "Bad result value for valid insert"); - a = assert.commandWorked( - db.adminCommand({doTxn: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]})); + a = assert.commandWorked(db.adminCommand({ + doTxn: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}], + txnNumber: NumberLong(txnNumber++) + })); assert.eq(1, t.find().count(), "Duplicate insert failed"); assert.eq(true, a.results[0], "Bad result value for duplicate insert"); @@ -146,14 +227,17 @@ assert.eq(o, t.findOne(), "Mismatching document inserted."); // 'o' field is an empty array. - assert.commandFailed(db.adminCommand({doTxn: [{op: 'i', ns: t.getFullName(), o: []}]}), - 'doTxn should fail on insert of object with empty array element'); + assert.commandFailed( + db.adminCommand( + {doTxn: [{op: 'i', ns: t.getFullName(), o: []}], txnNumber: NumberLong(txnNumber++)}), + 'doTxn should fail on insert of object with empty array element'); var res = assert.commandWorked(db.runCommand({ doTxn: [ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 18}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 19}}} - ] + ], + txnNumber: NumberLong(txnNumber++) })); o.x++; @@ -170,7 +254,8 @@ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 20}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 21}}} ], - preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 19}}] + preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 19}}], + txnNumber: NumberLong(txnNumber++) })); o.x++; @@ -187,7 +272,8 @@ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 22}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 23}}} ], - preCondition: [{ns: "foo.otherName", q: {_id: 5}, res: {x: 21}}] + preCondition: [{ns: "foo.otherName", q: {_id: 5}, res: {x: 21}}], + txnNumber: NumberLong(txnNumber++) })); assert.eq(o, t.findOne(), "preCondition didn't match, but ops were still applied"); @@ -198,7 +284,8 @@ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 22}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 23}}} ], - preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 19}}] + preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 19}}], + txnNumber: NumberLong(txnNumber++) })); assert.eq(o, t.findOne(), "preCondition didn't match, but ops were still applied"); @@ -207,7 +294,8 @@ doTxn: [ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 22}}}, {op: "u", ns: t.getFullName(), o2: {_id: 6}, o: {$set: {x: 23}}} - ] + ], + txnNumber: NumberLong(txnNumber++) })); assert.eq(false, res.results[0], "Op required upsert, which should be disallowed."); @@ -219,7 +307,8 @@ doTxn: [ {"op": "i", "ns": t.getFullName(), "o": {_id: 6}}, {"op": "u", "ns": t.getFullName(), "o2": {_id: 6}, "o": {$set: {z: 1, a: 2}}} - ] + ], + txnNumber: NumberLong(txnNumber++) })); assert.eq(t.findOne({_id: 6}), {_id: 6, a: 2, z: 1}); // Note: 'a' and 'z' have been sorted. @@ -233,7 +322,8 @@ "o2": {_id: 7}, "o": {$v: NumberLong(0), $set: {z: 1, a: 2}} } - ] + ], + txnNumber: NumberLong(txnNumber++), })); assert.eq(res.code, 40682); @@ -248,7 +338,8 @@ "o2": {_id: 8}, "o": {$v: NumberLong(1), $set: {z: 1, a: 2}} } - ] + ], + txnNumber: NumberLong(txnNumber++), })); assert.eq(t.findOne({_id: 8}), {_id: 8, a: 2, z: 1}); // Note: 'a' and 'z' have been sorted. })(); diff --git a/jstests/core/do_txn_oneshot.js b/jstests/core/do_txn_oneshot.js new file mode 100644 index 00000000000..89bcb65b815 --- /dev/null +++ b/jstests/core/do_txn_oneshot.js @@ -0,0 +1,80 @@ +// @tags: [requires_non_retryable_commands] + +// Tests that doTxn produces correct oplog entries. +(function() { + 'use strict'; + // For isMMAPv1. + load("jstests/concurrency/fsm_workload_helpers/server_types.js"); + // For isReplSet + load("jstests/libs/fixture_helpers.js"); + load('jstests/libs/uuid_util.js'); + + if (isMMAPv1(db)) { + jsTestLog("Skipping test as the storage engine does not support doTxn."); + return; + } + if (!FixtureHelpers.isReplSet(db)) { + jsTestLog("Skipping test as doTxn requires a replSet and replication is not enabled."); + return; + } + + var oplog = db.getSiblingDB('local').oplog.rs; + var session = db.getMongo().startSession(); + var sessionDb = session.getDatabase("test"); + var t = db.doTxn; + t.drop(); + db.createCollection(t.getName()); + const tUuid = getUUIDFromListCollections(db, t.getName()); + + // + // Test insert ops. Insert ops are unmodified except the UUID field is added. + // + const insertOps = [ + {op: 'i', ns: t.getFullName(), o: {_id: 100, x: 1, y: 1}}, + {op: 'i', ns: t.getFullName(), o: {_id: 101, x: 2, y: 1}}, + ]; + assert.commandWorked(sessionDb.runCommand({doTxn: insertOps, txnNumber: NumberLong("1")})); + let topOfOplog = oplog.find().sort({$natural: -1}).limit(1).next(); + assert.eq(topOfOplog.txnNumber, NumberLong("1")); + assert.docEq(topOfOplog.o.applyOps, insertOps.map(x => Object.assign(x, {ui: tUuid}))); + + // + // Test update ops. For updates, the "$v" UpdateSemantics field is added and non-idempotent + // operations are made idempotent. + // + const updateOps = [ + {op: 'u', ns: t.getFullName(), o: {$inc: {x: 10}}, o2: {_id: 100}}, + {op: 'u', ns: t.getFullName(), o: {$inc: {x: 10}}, o2: {_id: 101}} + ]; + const expectedUpdateOps = [ + {op: 'u', ns: t.getFullName(), o: {$v: 1, $set: {x: 11}}, o2: {_id: 100}, ui: tUuid}, + {op: 'u', ns: t.getFullName(), o: {$v: 1, $set: {x: 12}}, o2: {_id: 101}, ui: tUuid} + ]; + assert.commandWorked(sessionDb.runCommand({doTxn: updateOps, txnNumber: NumberLong("2")})); + topOfOplog = oplog.find().sort({$natural: -1}).limit(1).next(); + assert.eq(topOfOplog.txnNumber, NumberLong("2")); + assert.docEq(topOfOplog.o.applyOps, expectedUpdateOps); + + // + // Test delete ops. Delete ops are unmodified except the UUID field is added. + // + const deleteOps = [ + {op: 'd', ns: t.getFullName(), o: {_id: 100}}, + {op: 'd', ns: t.getFullName(), o: {_id: 101}} + ]; + assert.commandWorked(sessionDb.runCommand({doTxn: deleteOps, txnNumber: NumberLong("3")})); + topOfOplog = oplog.find().sort({$natural: -1}).limit(1).next(); + assert.eq(topOfOplog.txnNumber, NumberLong("3")); + assert.docEq(topOfOplog.o.applyOps, deleteOps.map(x => Object.assign(x, {ui: tUuid}))); + + // + // Make sure the transaction table is not affected by one-shot transactions. + // + assert.eq(0, + db.getSiblingDB('config') + .transactions.find({"_id.id": session.getSessionId().id}) + .toArray(), + "No transactions should be written to the transaction table."); + + session.endSession(); +})(); diff --git a/jstests/core/do_txn_atomicity.js b/jstests/core/do_txn_oplog_entry.js index a1c54f5dd7e..23be1c4d819 100644 --- a/jstests/core/do_txn_atomicity.js +++ b/jstests/core/do_txn_oplog_entry.js @@ -6,47 +6,60 @@ // For isMMAPv1. load("jstests/concurrency/fsm_workload_helpers/server_types.js"); + // For isReplSet + load("jstests/libs/fixture_helpers.js"); if (isMMAPv1(db)) { jsTestLog("Skipping test as the storage engine does not support doTxn."); return; } + if (!FixtureHelpers.isReplSet(db)) { + jsTestLog("Skipping test as doTxn requires a replSet and replication is not enabled."); + return; + } + + var session = db.getMongo().startSession(); + var sessionDb = session.getDatabase("test"); + var txnNumber = 0; var t = db.doTxn; t.drop(); assert.writeOK(t.insert({_id: 1})); // Operations including commands are not allowed and should be rejected completely. - assert.commandFailedWithCode(db.adminCommand({ + assert.commandFailedWithCode(sessionDb.adminCommand({ doTxn: [ {op: 'i', ns: t.getFullName(), o: {_id: ObjectId(), x: 1}}, {op: 'c', ns: "invalid", o: {create: "t"}}, - ] + ], + txnNumber: NumberLong(txnNumber++) }), ErrorCodes.InvalidOptions); assert.eq(t.count({x: 1}), 0); // Operations only including CRUD commands should be atomic, so the next insert will fail. var tooLong = Array(2000).join("hello"); - assert.commandFailedWithCode(db.adminCommand({ + assert.commandFailedWithCode(sessionDb.adminCommand({ doTxn: [ {op: 'i', ns: t.getFullName(), o: {_id: ObjectId(), x: 1}}, {op: 'i', ns: t.getFullName(), o: {_id: tooLong, x: 1}}, - ] + ], + txnNumber: NumberLong(txnNumber++) }), ErrorCodes.KeyTooLong); assert.eq(t.count({x: 1}), 0); // Operations on non-existent databases cannot be atomic. var newDBName = "do_txn_atomicity"; - var newDB = db.getSiblingDB(newDBName); + var newDB = sessionDb.getSiblingDB(newDBName); assert.commandWorked(newDB.dropDatabase()); // Updates on a non-existent database no longer implicitly create collections and will fail with // a NamespaceNotFound error. - assert.commandFailedWithCode( - newDB.runCommand( - {doTxn: [{op: "u", ns: newDBName + ".foo", o: {_id: 5, x: 17}, o2: {_id: 5, x: 16}}]}), - ErrorCodes.NamespaceNotFound); + assert.commandFailedWithCode(newDB.runCommand({ + doTxn: [{op: "u", ns: newDBName + ".foo", o: {_id: 5, x: 17}, o2: {_id: 5, x: 16}}], + txnNumber: NumberLong(txnNumber++) + }), + ErrorCodes.NamespaceNotFound); var sawTooManyLocksError = false; @@ -66,7 +79,8 @@ multiOps.push({op: 'i', ns: newDBName + "." + multiName, o: {_id: 0, x: [0, 1]}}); } - let res = [cappedOps, multiOps].map((doTxn) => newDB.runCommand({doTxn})); + let res = [cappedOps, multiOps].map( + (doTxn) => newDB.runCommand({doTxn: doTxn, txnNumber: NumberLong(txnNumber++)})); sawTooManyLocksError |= res.some((res) => res.code === ErrorCodes.TooManyLocks); // Transactions involving just two collections should succeed. if (n <= 2) diff --git a/jstests/core/json_schema/misc_validation.js b/jstests/core/json_schema/misc_validation.js index 23ce4ef72c1..29504be9d69 100644 --- a/jstests/core/json_schema/misc_validation.js +++ b/jstests/core/json_schema/misc_validation.js @@ -25,6 +25,8 @@ // For isMMAPv1. load("jstests/concurrency/fsm_workload_helpers/server_types.js"); + // For isReplSet + load("jstests/libs/fixture_helpers.js"); const testName = "json_schema_misc_validation"; const testDB = db.getSiblingDB(testName); @@ -319,9 +321,11 @@ coll.drop(); assert.writeOK(coll.insert({_id: 1, a: true})); - if (!isMMAPv1(db)) { + if (FixtureHelpers.isReplSet(db) && !isMongos && !isMMAPv1(db)) { // Test $jsonSchema in the precondition checking for doTxn. - res = testDB.adminCommand({ + const session = db.getMongo().startSession(); + const sessionDb = session.getDatabase(testDB.getName()); + res = sessionDb.adminCommand({ doTxn: [ {op: "u", ns: coll.getFullName(), o2: {_id: 1}, o: {$set: {a: false}}}, ], @@ -329,7 +333,8 @@ ns: coll.getFullName(), q: {$jsonSchema: {properties: {a: {type: "boolean"}}}}, res: {a: true} - }] + }], + txnNumber: NumberLong("0") }); assert.commandWorked(res); assert.eq(1, res.applied); diff --git a/jstests/core/views/views_all_commands.js b/jstests/core/views/views_all_commands.js index 696e72fff80..e5f11d2c1b5 100644 --- a/jstests/core/views/views_all_commands.js +++ b/jstests/core/views/views_all_commands.js @@ -190,10 +190,17 @@ delete: {command: {delete: "view", deletes: [{q: {x: 1}, limit: 1}]}, expectFailure: true}, distinct: {command: {distinct: "view", key: "_id"}}, doTxn: { - command: {doTxn: [{op: "i", o: {_id: 1}, ns: "test.view"}]}, + command: { + doTxn: [{op: "i", o: {_id: 1}, ns: "test.view"}], + txnNumber: NumberLong("0"), + lsid: {id: UUID()} + }, expectFailure: true, - expectedErrorCode: - [ErrorCodes.CommandNotSupportedOnView, ErrorCodes.CommandNotSupported], + expectedErrorCode: [ + ErrorCodes.CommandNotSupportedOnView, + ErrorCodes.CommandNotSupported, + ErrorCodes.IllegalOperation + ], skipSharded: true, }, driverOIDTest: {skip: isUnrelated}, |