/*-
 * Public Domain 2014-2018 MongoDB, Inc.
 * Public Domain 2008-2014 WiredTiger, Inc.
 *
 * This is free and unencumbered software released into the public domain.
 *
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 *
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
#include "test_util.h"

/*
 * JIRA ticket reference: WT-3363
 *
 * Test case description: There are a number of operations that we run that we
 * expect not to conflict with or block against a running checkpoint. This test
 * aims to run repeated checkpoints in a thread, while running an assortment
 * of operations that we expect to execute quickly on further threads. To
 * ensure that we catch any blockages we introduce a very large delay into the
 * checkpoint and measure that no operation takes 1/2 the length of this delay.
 *
 * Failure mode: We monitor the execution time of all operations and if we find
 * any operation taking longer than 1/2 the delay time, we abort dumping a core
 * file which can be used to determine what operation was blocked.
 */
static WT_THREAD_RET do_checkpoints(void *);
static WT_THREAD_RET do_ops(void *);
static WT_THREAD_RET monitor(void *);

/*
 * Time delay to introduce into checkpoints in seconds. Should be at-least
 * double the maximum time that any one of the operations should take. Currently
 * this is set to 10 seconds and we expect no single operation to take longer
 * than 5 seconds.
 */
#define	MAX_EXECUTION_TIME	10
#define	N_THREADS		10

/*
 * Number of seconds to execute for. Initially set to 15 minutes, as we need to
 * run long enough to be certain we have captured any blockages. In initial
 * testing 5 minutes was enough to reproduce the issue, so we run for 3x that
 * here to ensure we reproduce before declaring success.
 */
#define	RUNTIME	900.0

static WT_EVENT_HANDLER event_handler = {
	handle_op_error,
	handle_op_message,
	NULL,
	NULL
};

int
main(int argc, char *argv[])
{
	TEST_OPTS *opts, _opts;
	TEST_PER_THREAD_OPTS thread_args[N_THREADS];
	pthread_t ckpt_thread, mon_thread, threads[N_THREADS];
	int i;

	/*
	 * This test should not run unless long tests flag is set. The test
	 * runs for 15 minutes.
	 */
	if (!testutil_is_flag_set("TESTUTIL_ENABLE_TIMING_TESTS"))
		return (EXIT_SUCCESS);

	opts = &_opts;
	opts->unique_id = 0;
	memset(opts, 0, sizeof(*opts));

	testutil_check(testutil_parse_opts(argc, argv, opts));
	testutil_make_work_dir(opts->home);

	testutil_check(wiredtiger_open(opts->home, &event_handler,
	    "create,cache_size=1G,timing_stress_for_test=[checkpoint_slow]",
	    &opts->conn));

	testutil_check(pthread_create(
	    &ckpt_thread, NULL, do_checkpoints, opts));

	for (i = 0; i < N_THREADS; ++i) {
		thread_args[i].testopts = opts;
		thread_args[i].thread_counter = 0;
		thread_args[i].threadnum = i;
		testutil_check(pthread_create(
		    &threads[i], NULL, do_ops, &thread_args[i]));
	}

	/*
	 * Pass the whole array of thread arguments to the monitoring thread.
	 * This thread will need to monitor each threads counter to track if it
	 * is stuck.
	 */
	testutil_check(pthread_create(&mon_thread, NULL, monitor, thread_args));

	for (i = 0; i < N_THREADS; ++i)
		testutil_check(pthread_join(threads[i], NULL));

	testutil_check(pthread_join(mon_thread, NULL));

	testutil_check(pthread_join(ckpt_thread, NULL));

	printf("Success\n");

	testutil_cleanup(opts);
	return (EXIT_SUCCESS);
}

/*
 * Function for repeatedly running checkpoint operations.
 */
static WT_THREAD_RET
do_checkpoints(void *_opts)
{
	TEST_OPTS *opts;
	WT_SESSION *session;
	time_t now, start;
	int ret;

	opts = (TEST_OPTS *)_opts;
	(void)time(&start);
	(void)time(&now);

	while (difftime(now, start) < RUNTIME) {
		testutil_check(
		    opts->conn->open_session(opts->conn, NULL, NULL, &session));

		if ((ret = session->checkpoint(session, "force")) != 0)
			if (ret != EBUSY && ret != ENOENT)
				testutil_die(ret, "session.checkpoint");

		testutil_check(session->close(session, NULL));

		/*
		 * A short sleep to let operations process and avoid back to
		 * back checkpoints locking up resources.
		 */
		sleep(1);
		(void)time(&now);
	}

	return (WT_THREAD_RET_VALUE);
}

/*
 * Function to monitor running operations and abort to dump core in the event
 * that we catch an operation running long.
 */
static WT_THREAD_RET
monitor(void *args)
{
	TEST_PER_THREAD_OPTS *thread_args;
	time_t now, start;
	int ctr, i, last_ops[N_THREADS];

	thread_args = (TEST_PER_THREAD_OPTS *)args;

	(void)time(&start);
	(void)time(&now);

	memset(last_ops, 0, sizeof(int) + N_THREADS);

	while (difftime(now, start) < RUNTIME) {
		/*
		 * Checkpoints will run for slightly over MAX_EXECUTION_TIME.
		 * MAX_EXECUTION_TIME should always be long enough that we can
		 * complete any single operation in 1/2 that time.
		 */
		sleep(MAX_EXECUTION_TIME / 2);

		for (i = 0; i < N_THREADS; i++) {
			ctr = thread_args[i].thread_counter;

			/* Ignore any threads which may not have started yet. */
			if (ctr == 0)
				continue;

			/*
			 * We track how many operations each thread has done. If
			 * we have slept and the counter remains the same for a
			 * thread it is stuck and should drop a core so the
			 * cause of the hang can be investigated.
			 */
			if (ctr != last_ops[i])
				last_ops[i] = ctr;
			else {
				printf("Thread %d had a task running"
				    " for more than %d seconds\n",
				    i, MAX_EXECUTION_TIME / 2);
				abort();
			}
		}
		(void)time(&now);
	}

	return (WT_THREAD_RET_VALUE);
}

/*
 * Worker thread. Executes random operations from the set of 6.
 */
static WT_THREAD_RET
do_ops(void *args)
{
	WT_RAND_STATE rnd;
	time_t now, start;

	__wt_random_init_seed(NULL, &rnd);
	(void)time(&start);
	(void)time(&now);

	while (difftime(now, start) < RUNTIME) {
		switch (__wt_random(&rnd) % 6) {
		case 0:
			op_bulk(args);
			break;
		case 1:
			op_create(args);
			break;
		case 2:
			op_cursor(args);
			break;
		case 3:
			op_drop(args);
			break;
		case 4:
			op_bulk_unique(args);
			break;
		case 5:
			op_create_unique(args);
			break;
		}
		(void)time(&now);
	}

	return (WT_THREAD_RET_VALUE);
}
