// Test mongos implementation of time-limited operations: verify that mongos correctly forwards max
// time to shards, and that mongos correctly times out max time sharded getmore operations (which
// are run in series on shards).
//
// Note that mongos does not time out commands or query ops (which remains responsibility of mongod,
// pending development of an interrupt framework for mongos).

var st = new ShardingTest({shards: 2});
st.stopBalancer();
var mongos = st.s0;
var shards = [st.shard0, st.shard1];
var coll = mongos.getCollection("foo.bar");
var admin = mongos.getDB("admin");
var exceededTimeLimit = 50; // ErrorCodes::ExceededTimeLimit
var cursor;
var res;

// Helper function to configure "maxTimeAlwaysTimeOut" fail point on shards, which forces mongod to
// throw if it receives an operation with a max time.  See fail point declaration for complete
// description.
var configureMaxTimeAlwaysTimeOut = function(mode) {
    assert.commandWorked(shards[0].getDB("admin").runCommand(
        {configureFailPoint: "maxTimeAlwaysTimeOut", mode: mode}));
    assert.commandWorked(shards[1].getDB("admin").runCommand(
        {configureFailPoint: "maxTimeAlwaysTimeOut", mode: mode}));
}

// Helper function to configure "maxTimeAlwaysTimeOut" fail point on shards, which prohibits mongod
// from enforcing time limits.  See fail point declaration for complete description.
var configureMaxTimeNeverTimeOut = function(mode) {
    assert.commandWorked(shards[0].getDB("admin").runCommand(
        {configureFailPoint: "maxTimeNeverTimeOut", mode: mode}));
    assert.commandWorked(shards[1].getDB("admin").runCommand(
        {configureFailPoint: "maxTimeNeverTimeOut", mode: mode}));
}

//
// Pre-split collection: shard 0 takes {_id: {$lt: 0}}, shard 1 takes {_id: {$gte: 0}}.
//
assert.commandWorked(admin.runCommand({enableSharding: coll.getDB().getName()}));
assert.commandWorked(admin.runCommand({movePrimary: coll.getDB().getName(),
                                       to: "shard0000"}));
assert.commandWorked(admin.runCommand({shardCollection: coll.getFullName(),
                                       key: {_id: 1}}));
assert.commandWorked(admin.runCommand({split: coll.getFullName(), middle: {_id: 0}}));
assert.commandWorked(admin.runCommand({moveChunk: coll.getFullName(),
                                       find: {_id: 0},
                                       to: "shard0001"}));

//
// Insert 100 documents into sharded collection, such that each shard owns 50.
//
var bulk = coll.initializeUnorderedBulkOp();
for (i=-50; i<50; i++) {
    bulk.insert({ _id: i });
}
assert.writeOK(bulk.execute());
assert.eq(50, shards[0].getCollection(coll.getFullName()).count());
assert.eq(50, shards[1].getCollection(coll.getFullName()).count());


//
// Test that mongos correctly forwards max time to shards for sharded queries.  Uses
// maxTimeAlwaysTimeOut to ensure mongod throws if it receives a max time.
//

// Positive test.
configureMaxTimeAlwaysTimeOut("alwaysOn");
cursor = coll.find();
cursor.maxTimeMS(60*1000);
assert.throws(function() { cursor.next() },
              [],
              "expected query to fail in mongod due to maxTimeAlwaysTimeOut fail point");

// Negative test.
configureMaxTimeAlwaysTimeOut("off");
cursor = coll.find();
cursor.maxTimeMS(60*1000);
assert.doesNotThrow(function() { cursor.next() },
                    [],
                    "expected query to not hit time limit in mongod");

//
// Test that mongos correctly times out max time sharded getmore operations.  Uses
// maxTimeNeverTimeOut to ensure mongod doesn't enforce a time limit.
//

configureMaxTimeNeverTimeOut("alwaysOn");

// Positive test.  ~10s operation, 5s limit.  The operation takes ~10s because each shard processes
// 25 batches of ~200ms each, and mongos never runs getmore in parallel on shards.
cursor = coll.find({$where: function() { sleep(100); return true; }});
cursor.batchSize(2);
cursor.maxTimeMS(5*1000);
assert.doesNotThrow(function() { cursor.next(); },
                    [],
                    "did not expect mongos to time out first batch of query");
assert.throws(function() { cursor.itcount(); },
              [],
              "expected mongos to abort getmore due to time limit");

// Negative test.  Same as above (~10s operation), with a high (1-day) limit.
cursor = coll.find({$where: function() { sleep(100); return true; }});
cursor.batchSize(2);
cursor.maxTimeMS(1000*60*60*24);
assert.doesNotThrow(function() { cursor.next(); },
                    [],
                    "did not expect mongos to time out first batch of query");
assert.doesNotThrow(function() { cursor.itcount(); },
                    [],
                    "did not expect getmore ops to hit the time limit");

configureMaxTimeNeverTimeOut("off");

//
// Test that mongos correctly forwards max time to shards for sharded commands.  Uses
// maxTimeAlwaysTimeOut to ensure mongod throws if it receives a max time.
//

// Positive test for "validate".
configureMaxTimeAlwaysTimeOut("alwaysOn");
res = coll.runCommand("validate", {maxTimeMS: 60*1000});
assert.commandFailed(res,
                     "expected validate to fail in mongod due to maxTimeAlwaysTimeOut fail point");
assert.eq(res["code"],
          exceededTimeLimit,
          "expected code " + exceededTimeLimit + " from validate, instead got: " + tojson(res));

// Negative test for "validate".
configureMaxTimeAlwaysTimeOut("off");
assert.commandWorked(coll.runCommand("validate", {maxTimeMS: 60*1000}),
                     "expected validate to not hit time limit in mongod");

// Positive test for "count".
configureMaxTimeAlwaysTimeOut("alwaysOn");
res = coll.runCommand("count", {maxTimeMS: 60*1000});
assert.commandFailed(res,
                     "expected count to fail in mongod due to maxTimeAlwaysTimeOut fail point");
assert.eq(res["code"],
          exceededTimeLimit,
          "expected code " + exceededTimeLimit + " from count , instead got: " + tojson(res));

// Negative test for "count".
configureMaxTimeAlwaysTimeOut("off");
assert.commandWorked(coll.runCommand("count", {maxTimeMS: 60*1000}),
                     "expected count to not hit time limit in mongod");

// Positive test for "collStats".
configureMaxTimeAlwaysTimeOut("alwaysOn");
res = coll.runCommand("collStats", {maxTimeMS: 60*1000});
assert.commandFailed(res,
                     "expected collStats to fail in mongod due to maxTimeAlwaysTimeOut fail point");
assert.eq(res["code"],
          exceededTimeLimit,
          "expected code " + exceededTimeLimit + " from collStats, instead got: " + tojson(res));

// Negative test for "collStats".
configureMaxTimeAlwaysTimeOut("off");
assert.commandWorked(coll.runCommand("collStats", {maxTimeMS: 60*1000}),
                     "expected collStats to not hit time limit in mongod");

// Positive test for "mapReduce".
configureMaxTimeAlwaysTimeOut("alwaysOn");
res = coll.runCommand("mapReduce", {map: function() { emit(0, 0); },
                                    reduce: function(key, values) { return 0; },
                                    out: {inline: 1},
                                    maxTimeMS: 60*1000});
assert.commandFailed(res,
                     "expected mapReduce to fail in mongod due to maxTimeAlwaysTimeOut fail point");
assert.eq(res["code"],
          exceededTimeLimit,
          "expected code " + exceededTimeLimit + " from mapReduce, instead got: " + tojson(res));

// Negative test for "mapReduce".
configureMaxTimeAlwaysTimeOut("off");
assert.commandWorked(coll.runCommand("mapReduce", {map: function() { emit(0, 0); },
                                                   reduce: function(key, values) { return 0; },
                                                   out: {inline: 1},
                                                   maxTimeMS: 60*1000}),
                     "expected mapReduce to not hit time limit in mongod");

// Positive test for "aggregate".
configureMaxTimeAlwaysTimeOut("alwaysOn");
res = coll.runCommand("aggregate", {pipeline: [],
                                    maxTimeMS: 60*1000});
assert.commandFailed(res,
                     "expected aggregate to fail in mongod due to maxTimeAlwaysTimeOut fail point");
assert.eq(res["code"],
          exceededTimeLimit,
          "expected code " + exceededTimeLimit + " from aggregate , instead got: " + tojson(res));

// Negative test for "aggregate".
configureMaxTimeAlwaysTimeOut("off");
assert.commandWorked(coll.runCommand("aggregate", {pipeline: [],
                                                   maxTimeMS: 60*1000}),
                     "expected aggregate to not hit time limit in mongod");

// Positive test for "moveChunk".
configureMaxTimeAlwaysTimeOut("alwaysOn");
res = admin.runCommand({moveChunk: coll.getFullName(),
                        find: {_id: 0},
                        to: "shard0000",
                        maxTimeMS: 1000*60*60*24});
assert.commandFailed(res,
                     "expected moveChunk to fail in mongod due to maxTimeAlwaysTimeOut fail point");
assert.eq(res["code"],
          exceededTimeLimit,
          "expected code " + exceededTimeLimit + " from moveChunk, instead got: " + tojson(res));

// Negative test for "moveChunk".
configureMaxTimeAlwaysTimeOut("off");
assert.commandWorked(admin.runCommand({moveChunk: coll.getFullName(),
                                       find: {_id: 0},
                                       to: "shard0000",
                                       maxTimeMS: 1000*60*60*24}),
                     "expected moveChunk to not hit time limit in mongod");

// TODO Test additional commmands.

st.stop();
