mongo/jstests/replsets/read_committed.js

177 lines
6.4 KiB
JavaScript

/**
* Test basic read committed functionality, including:
* - Writes with writeConcern 'majority' should be visible once the write completes.
* - With the only data-bearing secondary down, committed reads should not include newly inserted
* data.
* - When data-bearing node comes back up and catches up, writes should be readable.
*
* @tags: [requires_majority_read_concern]
*/
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {restartServerReplication, stopServerReplication} from "jstests/libs/write_concern_util.js";
const majorityWriteConcern = {
writeConcern: {w: "majority", wtimeout: 60 * 1000},
};
// Each test case includes a 'prepareCollection' method that sets up the initial state starting
// with an empty collection, a 'write' method that does some write, and two arrays,
// 'expectedBefore' and 'expectedAfter' that describe the expected contents of the collection
// before and after the write. The 'prepareCollection' and 'write' methods should leave the
// collection either empty or with a single document with _id: 1.
const testCases = {
insert: {
prepareCollection: function (coll) {}, // No-op
write: function (coll, writeConcern) {
assert.commandWorked(coll.insert({_id: 1}, writeConcern));
},
expectedBefore: [],
expectedAfter: [{_id: 1}],
},
update: {
prepareCollection: function (coll) {
assert.commandWorked(coll.insert({_id: 1, state: "before"}, majorityWriteConcern));
},
write: function (coll, writeConcern) {
assert.commandWorked(coll.update({_id: 1}, {$set: {state: "after"}}, writeConcern));
},
expectedBefore: [{_id: 1, state: "before"}],
expectedAfter: [{_id: 1, state: "after"}],
},
remove: {
prepareCollection: function (coll) {
assert.commandWorked(coll.insert({_id: 1}, majorityWriteConcern));
},
write: function (coll, writeConcern) {
assert.commandWorked(coll.remove({_id: 1}, writeConcern));
},
expectedBefore: [{_id: 1}],
expectedAfter: [],
},
};
// Set up a set and grab things for later.
let name = "read_committed";
let replTest = new ReplSetTest({name: name, nodes: 3});
replTest.startSet();
let nodes = replTest.nodeList();
let config = {
"_id": name,
"members": [
{"_id": 0, "host": nodes[0]},
{"_id": 1, "host": nodes[1], priority: 0},
{"_id": 2, "host": nodes[2], arbiterOnly: true},
],
};
replTest.initiate(config);
// Get connections and collection.
let primary = replTest.getPrimary();
let secondary = replTest.getSecondary();
let coll = primary.getDB(name)[name];
let secondaryColl = secondary.getDB(name)[name];
// The default WC is majority and stopServerReplication will prevent satisfying any majority writes.
assert.commandWorked(
primary.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}, writeConcern: {w: "majority"}}),
);
replTest.awaitReplication();
function log(arg) {
jsTest.log(tojson(arg));
}
function doRead(coll, readConcern) {
readConcern.maxTimeMS = 3000;
let res = assert.commandWorked(coll.runCommand("find", readConcern));
return new DBCommandCursor(coll.getDB(), res).toArray();
}
function doDirtyRead(coll) {
log("doing dirty read");
let ret = doRead(coll, {"readConcern": {"level": "local"}});
log("done doing dirty read.");
return ret;
}
function doCommittedRead(coll) {
log("doing committed read");
let ret = doRead(coll, {"readConcern": {"level": "majority"}});
log("done doing committed read.");
return ret;
}
function readLatestOplogEntry(readConcernLevel) {
let oplog = primary.getDB("local").oplog.rs;
let res = oplog.runCommand("find", {
"readConcern": {"level": readConcernLevel},
"maxTimeMS": 3000,
sort: {$natural: -1},
limit: 1,
});
assert.commandWorked(res);
return new DBCommandCursor(coll.getDB(), res).toArray()[0];
}
for (let testName in testCases) {
jsTestLog("Running test " + testName);
var test = testCases[testName];
const setUpInitialState = function setUpInitialState() {
assert.commandWorked(coll.remove({}, majorityWriteConcern));
test.prepareCollection(coll);
// Do some sanity checks.
assert.eq(doDirtyRead(coll), test.expectedBefore);
assert.eq(doCommittedRead(coll), test.expectedBefore);
};
// Writes done with majority write concern must be immediately visible to both dirty and
// committed reads.
setUpInitialState();
test.write(coll, majorityWriteConcern);
assert.eq(doDirtyRead(coll), test.expectedAfter);
assert.eq(doCommittedRead(coll), test.expectedAfter);
// Return to the initial state, then stop the secondary from applying new writes to prevent
// them from becoming committed.
setUpInitialState();
stopServerReplication(secondary);
const initialOplogTs = readLatestOplogEntry("local").ts;
// Writes done without majority write concern must be immediately visible to dirty read
// and hidden from committed reads until they have been replicated. The rules for seeing
// an oplog entry for a write are the same as for the write itself.
test.write(coll, {});
assert.eq(doDirtyRead(coll), test.expectedAfter);
assert.neq(readLatestOplogEntry("local").ts, initialOplogTs);
assert.eq(doCommittedRead(coll), test.expectedBefore);
assert.eq(readLatestOplogEntry("majority").ts, initialOplogTs);
// Try the committed read again after sleeping to ensure it doesn't only work for
// queries immediately after the write.
sleep(1000);
assert.eq(doCommittedRead(coll), test.expectedBefore);
assert.eq(readLatestOplogEntry("majority").ts, initialOplogTs);
// Restart oplog application on the secondary and ensure the committed view is updated.
restartServerReplication(secondary);
replTest.awaitLastOpCommitted();
assert.eq(doCommittedRead(coll), test.expectedAfter);
assert.neq(readLatestOplogEntry("majority").ts, initialOplogTs);
// The secondary will be able to make the write committed soon after the primary, but there
// is no way to block until it does.
try {
assert.soon(function () {
return friendlyEqual(doCommittedRead(secondaryColl), test.expectedAfter);
});
} catch (e) {
// generate useful error messages on failures.
assert.eq(doCommittedRead(secondaryColl), test.expectedAfter);
}
}
replTest.stopSet();