load('jstests/libs/analyze_plan.js');

(function() {
    "use strict";

    // This test needs its own mongod since the snapshot names must be in increasing order and once
    // you
    // have a majority commit point it is impossible to go back to not having one.
    var testServer =
        MongoRunner.runMongod({setParameter: 'testingSnapshotBehaviorInIsolation=true'});
    var db = testServer.getDB("test");
    var t = db.readMajority;

    function assertNoReadMajoritySnapshotAvailable() {
        var res = t.runCommand('find',
                               {batchSize: 2, readConcern: {level: "majority"}, maxTimeMS: 1000});
        assert.commandFailed(res);
        assert.eq(res.code, ErrorCodes.ExceededTimeLimit);
    }

    function getReadMajorityCursor() {
        var res = t.runCommand('find', {batchSize: 2, readConcern: {level: "majority"}});
        assert.commandWorked(res);
        return new DBCommandCursor(db.getMongo(), res, 2);
    }

    function getReadMajorityAggCursor() {
        var res =
            t.runCommand('aggregate', {cursor: {batchSize: 2}, readConcern: {level: "majority"}});
        assert.commandWorked(res);
        return new DBCommandCursor(db.getMongo(), res, 2);
    }

    function getExplainPlan(query) {
        var res = db.runCommand({explain: {find: t.getName(), filter: query}});
        return assert.commandWorked(res).queryPlanner.winningPlan;
    }

    //
    // Actual Test
    //

    if (!db.serverStatus().storageEngine.supportsCommittedReads) {
        print("Skipping read_majority.js since storageEngine doesn't support it.");
        return;
    }

    // Ensure killOp will work on an op that is waiting for snapshots to be created
    var blockedReader = startParallelShell(
        "db.readMajority.runCommand('find', {batchSize: 2, readConcern: {level: 'majority'}});",
        testServer.port);

    assert.soon(function() {
        var curOps = db.currentOp(true);
        jsTestLog("curOp output: " + tojson(curOps));
        for (var i in curOps.inprog) {
            var op = curOps.inprog[i];
            if (op.op === 'query' && op.ns === "test.$cmd" && op.query.find === 'readMajority') {
                db.killOp(op.opid);
                return true;
            }
        }
        return false;
    }, "could not kill an op that was waiting for a snapshot", 60 * 1000);
    blockedReader();

    var snapshot1 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assert.commandWorked(db.runCommand({create: "readMajority"}));
    var snapshot2 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;

    for (var i = 0; i < 10; i++) {
        assert.writeOK(t.insert({_id: i, version: 3}));
    }

    assertNoReadMajoritySnapshotAvailable();

    var snapshot3 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;

    assertNoReadMajoritySnapshotAvailable();

    assert.writeOK(t.update({}, {$set: {version: 4}}, false, true));
    var snapshot4 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;

    // Collection didn't exist in snapshot 1.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot1}));
    assertNoReadMajoritySnapshotAvailable();

    // Collection existed but was empty in snapshot 2.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot2}));
    assert.eq(getReadMajorityCursor().itcount(), 0);
    assert.eq(getReadMajorityAggCursor().itcount(), 0);

    // In snapshot 3 the collection was filled with {version: 3} documents.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot3}));
    assert.eq(getReadMajorityAggCursor().itcount(), 10);
    getReadMajorityAggCursor().forEach(function(doc) {
        // Note: agg uses internal batching so can't reliably test flipping snapshot. However, it
        // uses
        // the same mechanism as find, so if one works, both should.
        assert.eq(doc.version, 3);
    });

    assert.eq(getReadMajorityCursor().itcount(), 10);
    var cursor = getReadMajorityCursor();  // Note: uses batchsize=2.
    assert.eq(cursor.next().version, 3);
    assert.eq(cursor.next().version, 3);
    assert(!cursor.objsLeftInBatch());

    // In snapshot 4 the collection was filled with {version: 3} documents.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot4}));

    // This triggers a getMore which sees the new version.
    assert.eq(cursor.next().version, 4);
    assert.eq(cursor.next().version, 4);

    // Adding an index bumps the min snapshot for a collection as of SERVER-20260. This may change
    // to
    // just filter that index out from query planning as part of SERVER-20439.
    t.ensureIndex({version: 1});
    assertNoReadMajoritySnapshotAvailable();

    // To use the index, a snapshot created after the index was completed must be marked committed.
    var newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);
    assert.eq(getReadMajorityAggCursor().itcount(), 10);
    assert(isIxscan(getExplainPlan({version: 1})));

    // Dropping an index does bump the min snapshot.
    t.dropIndex({version: 1});
    assertNoReadMajoritySnapshotAvailable();

    // To use the collection again, a snapshot created after the dropIndex must be marked committed.
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);

    // Reindex bumps the min snapshot.
    t.reIndex();
    assertNoReadMajoritySnapshotAvailable();
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);

    // Repair bumps the min snapshot.
    db.repairDatabase();
    assertNoReadMajoritySnapshotAvailable();
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);
    assert.eq(getReadMajorityAggCursor().itcount(), 10);

    // Dropping the collection is visible in the committed snapshot, even though it hasn't been
    // marked
    // committed yet. This is allowed by the current specification even though it violates strict
    // read-committed semantics since we don't guarantee them on metadata operations.
    t.drop();
    assert.eq(getReadMajorityCursor().itcount(), 0);
    assert.eq(getReadMajorityAggCursor().itcount(), 0);

    // Creating a new collection with the same name hides the collection until that operation is in
    // the
    // committed view.
    t.insert({_id: 0, version: 8});
    assertNoReadMajoritySnapshotAvailable();
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 1);
    assert.eq(getReadMajorityAggCursor().itcount(), 1);

    // Commands that only support read concern 'local', (such as ping) must work when it is
    // explicitly
    // specified and fail when 'majority' is specified.
    assert.commandWorked(db.adminCommand({ping: 1, readConcern: {level: 'local'}}));
    var res = assert.commandFailed(db.adminCommand({ping: 1, readConcern: {level: 'majority'}}));
    assert.eq(res.code, ErrorCodes.InvalidOptions);

    // Agg $out also doesn't support read concern majority.
    assert.commandWorked(
        t.runCommand('aggregate', {pipeline: [{$out: 'out'}], readConcern: {level: 'local'}}));
    var res = assert.commandFailed(
        t.runCommand('aggregate', {pipeline: [{$out: 'out'}], readConcern: {level: 'majority'}}));
    assert.eq(res.code, ErrorCodes.InvalidOptions);

    MongoRunner.stopMongod(testServer);
}());
