(function() {
'use strict';

load('jstests/concurrency/fsm_libs/runner.js');  // for runner.internals
load('jstests/libs/discover_topology.js');       // For Topology and DiscoverTopology.

const validateExecutionOptions = runner.internals.validateExecutionOptions;
const prepareCollections = runner.internals.prepareCollections;
const WorkloadFailure = runner.internals.WorkloadFailure;
const throwError = runner.internals.throwError;
const setupWorkload = runner.internals.setupWorkload;
const teardownWorkload = runner.internals.teardownWorkload;
const setIterations = runner.internals.setIterations;
const setThreadCount = runner.internals.setThreadCount;
const loadWorkloadContext = runner.internals.loadWorkloadContext;

// Returns true if the workload's teardown succeeds and false if the workload's teardown fails.
function cleanupWorkload(workload, context, cluster, errors, header) {
    const phase = 'before workload ' + workload + ' teardown';

    try {
        teardownWorkload(workload, context, cluster);
    } catch (e) {
        errors.push(new WorkloadFailure(e.toString(), e.stack, 'main', header + ' Teardown'));
        return false;
    }

    return true;
}

function runWorkloads(workloads,
                      {cluster: clusterOptions = {}, execution: executionOptions = {}} = {}) {
    assert.gt(workloads.length, 0, 'need at least one workload to run');

    const executionMode = {serial: true};
    validateExecutionOptions(executionMode, executionOptions);
    Object.freeze(executionOptions);  // immutable after validation (and normalization)

    // Determine how strong to make assertions while simultaneously executing different
    // workloads.
    let assertLevel = AssertLevel.OWN_DB;
    if (clusterOptions.sameDB) {
        // The database is shared by multiple workloads, so only make the asserts that apply
        // when the collection is owned by an individual workload.
        assertLevel = AssertLevel.OWN_COLL;
    }
    if (clusterOptions.sameCollection) {
        // The collection is shared by multiple workloads, so only make the asserts that always
        // apply.
        assertLevel = AssertLevel.ALWAYS;
    }
    globalAssertLevel = assertLevel;

    const context = {};
    const applyMultipliers = true;
    loadWorkloadContext(workloads, context, executionOptions, applyMultipliers);

    // Constructing a Cluster instance calls its internal validateClusterOptions() function,
    // which fills in any properties that aren't explicitly present in 'clusterOptions'. We do
    // this before constructing a ThreadManager instance to make its dependency on the
    // 'clusterOptions' being filled in explicit.
    const cluster = new Cluster(clusterOptions);
    const threadMgr = new ThreadManager(clusterOptions, executionMode);

    Random.setRandomSeed(clusterOptions.seed);

    const errors = [];
    const cleanup = [];
    let teardownFailed = false;
    let startTime = Date.now();  // Initialize in case setupWorkload fails below.
    let totalTime;

    cluster.setup();

    jsTest.log('Workload(s) started: ' + workloads.join(' '));

    prepareCollections(workloads, context, cluster, clusterOptions, executionOptions);

    try {
        // Set up the thread manager for this set of workloads.
        startTime = Date.now();

        {
            const maxAllowedThreads = 100 * executionOptions.threadMultiplier;
            threadMgr.init(workloads, context, maxAllowedThreads);
        }

        // Call each workload's setup function.
        workloads.forEach(function(workload) {
            // Define "iterations" and "threadCount" properties on the workload's $config.data
            // object so that they can be used within its setup(), teardown(), and state
            // functions. This must happen after calling threadMgr.init() in case the thread
            // counts needed to be scaled down.
            setIterations(context[workload].config);
            setThreadCount(context[workload].config);

            setupWorkload(workload, context, cluster);
            cleanup.push(workload);
        });

        // Await replication after running the $config.setup() function when stepdowns are
        // permitted to ensure its effects aren't rolled back.
        if (cluster.isReplication() && executionOptions.stepdownFiles !== undefined) {
            cluster.awaitReplication();
        }

        // Transactions run at snapshot read concern unless defaultTransactionReadConcernLevel
        // is set to another level.
        const transactionsWouldUseSnapshotReadConcern =
            !TestData.hasOwnProperty("defaultTransactionReadConcernLevel") ||
            TestData.defaultTransactionReadConcernLevel === "snapshot";

        // Synchronize the cluster times across all routers if the tests will be overriden to
        // use transactions, so the earliest global snapshots chosen by each router will include
        // the effects of each setup function. This is only required for snapshot read concern.
        if (cluster.isSharded() && TestData.runInsideTransaction &&
            transactionsWouldUseSnapshotReadConcern) {
            cluster.synchronizeMongosClusterTimes();
        }

        // After the $config.setup() function has been called, it is safe for the stepdown
        // thread to start running. The main thread won't attempt to interact with the cluster
        // until all of the spawned worker threads have finished.
        //

        // Indicate that the stepdown thread can run. It is unnecessary for the stepdown thread
        // to indicate that it is going to start running because it will eventually after the
        // worker threads have started.
        if (executionOptions.stepdownFiles !== undefined) {
            writeFile(executionOptions.stepdownFiles.permitted, '');
        }

        // Since the worker threads may be running with causal consistency enabled, we set the
        // initial clusterTime and initial operationTime for the sessions they'll create so that
        // they are guaranteed to observe the effects of the workload's $config.setup() function
        // being called.
        if (typeof executionOptions.sessionOptions === 'object' &&
            executionOptions.sessionOptions !== null) {
            // We only start a session for the worker threads and never start one for the main
            // thread. We can therefore get the clusterTime and operationTime tracked by the
            // underlying DummyDriverSession through any DB instance (i.e. the "test" database
            // here was chosen arbitrarily).
            const session = cluster.getDB('test').getSession();

            // JavaScript objects backed by C++ objects (e.g. BSON values from a command
            // response) do not serialize correctly when passed through the Thread
            // constructor. To work around this behavior, we instead pass a stringified form of
            // the JavaScript object through the Thread constructor and use eval() to
            // rehydrate it.
            executionOptions.sessionOptions.initialClusterTime = tojson(session.getClusterTime());
            executionOptions.sessionOptions.initialOperationTime =
                tojson(session.getOperationTime());
        }

        try {
            try {
                // Start this set of worker threads.
                threadMgr.spawnAll(cluster, executionOptions);
                // Allow 20% of the threads to fail. This allows the workloads to run on
                // underpowered test hosts.
                threadMgr.checkFailed(0.2);
            } finally {
                // Threads must be joined before destruction, so do this even in the presence of
                // exceptions.
                errors.push(...threadMgr.joinAll().map(
                    e => new WorkloadFailure(
                        e.err, e.stack, e.tid, 'Foreground ' + e.workloads.join(' '))));
            }
        } finally {
            // Until we are guaranteed that the stepdown thread isn't running, it isn't safe for
            // the $config.teardown() function to be called. We should signal to resmoke.py that
            // the stepdown thread should stop running and wait for the stepdown thread to
            // signal that it has stopped.
            //
            // Signal to the stepdown thread to stop stepping down the cluster.
            if (executionOptions.stepdownFiles !== undefined) {
                writeFile(executionOptions.stepdownFiles.idleRequest, '');

                // Wait for the acknowledgement file to be created by the stepdown thread.
                assert.soonNoExcept(function() {
                    // The cat() function will throw an exception if the file isn't found.
                    cat(executionOptions.stepdownFiles.idleAck);
                    return true;
                }, "stepdown still in progress");
            }
        }
    } finally {
        if (cluster.shouldPerformContinuousStepdowns()) {
            cluster.reestablishConnectionsAfterFailover();
        }
        // Call each workload's teardown function. After all teardowns have completed check if
        // any of them failed.
        const cleanupResults = cleanup.map(
            workload => cleanupWorkload(workload, context, cluster, errors, 'Foreground'));
        teardownFailed = cleanupResults.some(success => (success === false));

        totalTime = Date.now() - startTime;
        jsTest.log('Workload(s) completed in ' + totalTime + ' ms: ' + workloads.join(' '));
    }

    // Throw any existing errors so that resmoke.py can abort its execution of the test suite.
    throwError(errors);

    cluster.teardown();
}

if (typeof db === 'undefined') {
    throw new Error(
        'resmoke_runner.js must be run with the mongo shell already connected to the database');
}

const clusterOptions = {
    replication: {enabled: false},
    sharded: {enabled: false},
};

// The TestData.discoverTopoloy is false when we only care about connecting to either a
// standalone or primary node in a replica set.
if (TestData.discoverTopology !== false) {
    const topology = DiscoverTopology.findConnectedNodes(db.getMongo());

    if (topology.type === Topology.kReplicaSet) {
        clusterOptions.replication.enabled = true;
        clusterOptions.replication.numNodes = topology.nodes.length;
    } else if (topology.type === Topology.kShardedCluster) {
        clusterOptions.replication.enabled = true;
        clusterOptions.sharded.enabled = true;
        clusterOptions.sharded.enableAutoSplit =
            TestData.hasOwnProperty('runningWithAutoSplit') ? TestData.runningWithAutoSplit : true;
        clusterOptions.sharded.enableBalancer =
            TestData.hasOwnProperty('runningWithBalancer') ? TestData.runningWithBalancer : true;
        clusterOptions.sharded.numMongos = topology.mongos.nodes.length;
        clusterOptions.sharded.numShards = Object.keys(topology.shards).length;
        clusterOptions.sharded.stepdownOptions = {};
        clusterOptions.sharded.stepdownOptions.configStepdown =
            TestData.runningWithConfigStepdowns || false;
        clusterOptions.sharded.stepdownOptions.shardStepdown =
            TestData.runningWithShardStepdowns || false;
    } else if (topology.type !== Topology.kStandalone) {
        throw new Error('Unrecognized topology format: ' + tojson(topology));
    }
}

clusterOptions.sameDB = TestData.sameDB;
clusterOptions.sameCollection = TestData.sameCollection;

let workloads = TestData.fsmWorkloads;

let sessionOptions = {};
if (TestData.runningWithCausalConsistency !== undefined) {
    // Explicit sessions are causally consistent by default, so causal consistency has to be
    // explicitly disabled.
    sessionOptions.causalConsistency = TestData.runningWithCausalConsistency;

    if (TestData.runningWithCausalConsistency) {
        sessionOptions.readPreference = {mode: 'secondary'};
    }
}

if (TestData.runningWithConfigStepdowns || TestData.runningWithShardStepdowns) {
    sessionOptions.retryWrites = true;
}

const executionOptions = {
    dbNamePrefix: TestData.dbNamePrefix || ""
};
const resmokeDbPathPrefix = TestData.resmokeDbPathPrefix || ".";

// The stepdown file names need to match the same construction as found in
// buildscripts/resmokelib/testing/hooks/stepdown.py.
if (TestData.useStepdownPermittedFile) {
    executionOptions.stepdownFiles = {
        permitted: resmokeDbPathPrefix + '/permitted',
        idleRequest: resmokeDbPathPrefix + '/idle_request',
        idleAck: resmokeDbPathPrefix + '/idle_ack',
    };
}

if (Object.keys(sessionOptions).length > 0 || TestData.runningWithSessions) {
    executionOptions.sessionOptions = sessionOptions;
}

runWorkloads(workloads, {cluster: clusterOptions, execution: executionOptions});
})();
