Newer
Older
/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2012-2013, Digium, Inc.
*
* Mark Michelson <mmichelson@digium.com>
*
* See http://www.asterisk.org for more information about
* the Asterisk project. Please do not directly contact
* any of the maintainers of this project for assistance;
* the project provides a web site, mailing lists and IRC
* channels for your use.
*
* This program is free software, distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
/*!
* \file
* \brief threadpool unit tests
*
* \author Mark Michelson <mmichelson@digium.com>
*
*/
/*** MODULEINFO
<depend>TEST_FRAMEWORK</depend>
<support_level>core</support_level>
***/
#include "asterisk.h"
#include "asterisk/astobj2.h"
#include "asterisk/logger.h"
#include "asterisk/module.h"
#include "asterisk/taskprocessor.h"
#include "asterisk/test.h"
#include "asterisk/threadpool.h"
struct test_listener_data {
int num_active;
int num_idle;
int task_pushed;
int num_tasks;
int empty_notice;
int was_empty;
ast_mutex_t lock;
ast_cond_t cond;
};
static struct test_listener_data *test_alloc(void)
{
struct test_listener_data *tld = ast_calloc(1, sizeof(*tld));
if (!tld) {
return NULL;
}
ast_mutex_init(&tld->lock);
ast_cond_init(&tld->cond, NULL);
return tld;
}
static void test_state_changed(struct ast_threadpool *pool,
struct ast_threadpool_listener *listener,
int active_threads,
int idle_threads)
{
struct test_listener_data *tld = ast_threadpool_listener_get_user_data(listener);
SCOPED_MUTEX(lock, &tld->lock);
tld->num_active = active_threads;
tld->num_idle = idle_threads;
ast_log(LOG_NOTICE, "Thread state: %d active, %d idle\n", tld->num_active, tld->num_idle);
ast_cond_signal(&tld->cond);
}
static void test_task_pushed(struct ast_threadpool *pool,
struct ast_threadpool_listener *listener,
int was_empty)
{
struct test_listener_data *tld = ast_threadpool_listener_get_user_data(listener);
SCOPED_MUTEX(lock, &tld->lock);
tld->task_pushed = 1;
++tld->num_tasks;
tld->was_empty = was_empty;
ast_cond_signal(&tld->cond);
}
static void test_emptied(struct ast_threadpool *pool,
struct ast_threadpool_listener *listener)
{
struct test_listener_data *tld = ast_threadpool_listener_get_user_data(listener);
SCOPED_MUTEX(lock, &tld->lock);
tld->empty_notice = 1;
ast_cond_signal(&tld->cond);
}
static void test_shutdown(struct ast_threadpool_listener *listener)
struct test_listener_data *tld = ast_threadpool_listener_get_user_data(listener);
ast_cond_destroy(&tld->cond);
ast_mutex_destroy(&tld->lock);
}
static const struct ast_threadpool_listener_callbacks test_callbacks = {
.state_changed = test_state_changed,
.task_pushed = test_task_pushed,
.emptied = test_emptied,
.shutdown = test_shutdown,
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
};
struct simple_task_data {
int task_executed;
ast_mutex_t lock;
ast_cond_t cond;
};
static struct simple_task_data *simple_task_data_alloc(void)
{
struct simple_task_data *std = ast_calloc(1, sizeof(*std));
if (!std) {
return NULL;
}
ast_mutex_init(&std->lock);
ast_cond_init(&std->cond, NULL);
return std;
}
static int simple_task(void *data)
{
struct simple_task_data *std = data;
SCOPED_MUTEX(lock, &std->lock);
std->task_executed = 1;
ast_cond_signal(&std->cond);
return 0;
}
static enum ast_test_result_state wait_until_thread_state(struct ast_test *test, struct test_listener_data *tld, int num_active, int num_idle)
{
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + 5,
.tv_nsec = start.tv_usec * 1000
};
enum ast_test_result_state res = AST_TEST_PASS;
SCOPED_MUTEX(lock, &tld->lock);
while (!(tld->num_active == num_active && tld->num_idle == num_idle)) {
if (ast_cond_timedwait(&tld->cond, &tld->lock, &end) == ETIMEDOUT) {
break;
}
}
if (tld->num_active != num_active && tld->num_idle != num_idle) {
ast_test_status_update(test, "Number of active threads and idle threads not what was expected.\n");
ast_test_status_update(test, "Expected %d active threads but got %d\n", num_active, tld->num_active);
ast_test_status_update(test, "Expected %d idle threads but got %d\n", num_idle, tld->num_idle);
res = AST_TEST_FAIL;
}
return res;
}
static void wait_for_task_pushed(struct ast_threadpool_listener *listener)
{
struct test_listener_data *tld = ast_threadpool_listener_get_user_data(listener);
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + 5,
.tv_nsec = start.tv_usec * 1000
};
SCOPED_MUTEX(lock, &tld->lock);
while (!tld->task_pushed) {
if (ast_cond_timedwait(&tld->cond, lock, &end) == ETIMEDOUT) {
break;
}
static enum ast_test_result_state wait_for_completion(struct ast_test *test, struct simple_task_data *std)
{
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + 5,
.tv_nsec = start.tv_usec * 1000
};
enum ast_test_result_state res = AST_TEST_PASS;
SCOPED_MUTEX(lock, &std->lock);
while (!std->task_executed) {
if (ast_cond_timedwait(&std->cond, lock, &end) == ETIMEDOUT) {
break;
}
}
if (!std->task_executed) {
ast_test_status_update(test, "Task execution did not occur\n");
res = AST_TEST_FAIL;
}
return res;
}
static enum ast_test_result_state wait_for_empty_notice(struct ast_test *test, struct test_listener_data *tld)
{
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + 5,
.tv_nsec = start.tv_usec * 1000
};
enum ast_test_result_state res = AST_TEST_PASS;
SCOPED_MUTEX(lock, &tld->lock);
while (!tld->empty_notice) {
if (ast_cond_timedwait(&tld->cond, lock, &end) == ETIMEDOUT) {
break;
}
}
if (!tld->empty_notice) {
ast_test_status_update(test, "Test listener not notified that threadpool is empty\n");
res = AST_TEST_FAIL;
}
return res;
}
static enum ast_test_result_state listener_check(
struct ast_test *test,
struct ast_threadpool_listener *listener,
int task_pushed,
int was_empty,
int num_tasks,
int num_active,
int num_idle,
int empty_notice)
{
struct test_listener_data *tld = ast_threadpool_listener_get_user_data(listener);
enum ast_test_result_state res = AST_TEST_PASS;
if (tld->task_pushed != task_pushed) {
ast_test_status_update(test, "Expected task %sto be pushed, but it was%s\n",
task_pushed ? "" : "not ", tld->task_pushed ? "" : " not");
res = AST_TEST_FAIL;
}
if (tld->was_empty != was_empty) {
ast_test_status_update(test, "Expected %sto be empty, but it was%s\n",
was_empty ? "" : "not ", tld->was_empty ? "" : " not");
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
res = AST_TEST_FAIL;
}
if (tld->num_tasks!= num_tasks) {
ast_test_status_update(test, "Expected %d tasks to be pushed, but got %d\n",
num_tasks, tld->num_tasks);
res = AST_TEST_FAIL;
}
if (tld->num_active != num_active) {
ast_test_status_update(test, "Expected %d active threads, but got %d\n",
num_active, tld->num_active);
res = AST_TEST_FAIL;
}
if (tld->num_idle != num_idle) {
ast_test_status_update(test, "Expected %d idle threads, but got %d\n",
num_idle, tld->num_idle);
res = AST_TEST_FAIL;
}
if (tld->empty_notice != empty_notice) {
ast_test_status_update(test, "Expected %s empty notice, but got %s\n",
was_empty ? "an" : "no", tld->task_pushed ? "one" : "none");
res = AST_TEST_FAIL;
}
return res;
}
AST_TEST_DEFINE(threadpool_push)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct simple_task_data *std = NULL;
struct test_listener_data *tld = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "push";
info->category = "/main/threadpool/";
info->summary = "Test task";
info->description =
"Basic threadpool test";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
std = simple_task_data_alloc();
if (!std) {
goto end;
}
ast_threadpool_push(pool, simple_task, std);
wait_for_task_pushed(listener);
res = listener_check(test, listener, 1, 1, 1, 0, 0, 0);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(std);
AST_TEST_DEFINE(threadpool_initial_threads)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.auto_increment = 0,
.initial_size = 3,
};
switch (cmd) {
case TEST_INIT:
info->name = "initial_threads";
info->category = "/main/threadpool/";
info->summary = "Test threadpool initialization state";
info->description =
"Ensure that a threadpool created with a specific size contains the\n"
"proper number of idle threads.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 3);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
return res;
}
AST_TEST_DEFINE(threadpool_thread_creation)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "thread_creation";
info->category = "/main/threadpool/";
info->summary = "Test threadpool thread creation";
info->description =
"Ensure that threads can be added to a threadpool";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
/* Now let's create a thread. It should start active, then go
* idle immediately
*/
ast_threadpool_set_size(pool, 1);
res = wait_until_thread_state(test, tld, 0, 1);
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
return res;
}
AST_TEST_DEFINE(threadpool_thread_destruction)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "thread_destruction";
info->category = "/main/threadpool/";
info->summary = "Test threadpool thread destruction";
info->description =
"Ensure that threads are properly destroyed in a threadpool";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
ast_threadpool_set_size(pool, 3);
res = wait_until_thread_state(test, tld, 0, 3);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 0, 0, 0, 0, 3, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
ast_threadpool_set_size(pool, 2);
res = wait_until_thread_state(test, tld, 0, 2);
ast_threadpool_shutdown(pool);
AST_TEST_DEFINE(threadpool_thread_timeout)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 2,
.initial_size = 0,
};
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "thread_timeout";
info->category = "/main/threadpool/";
info->summary = "Test threadpool thread timeout";
info->description =
"Ensure that a thread with a two second timeout dies as expected.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
ast_threadpool_set_size(pool, 1);
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 0, 0, 0, 0, 1, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 0, 0, 0, 0, 0, 0);
end:
ast_threadpool_shutdown(pool);
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
AST_TEST_DEFINE(threadpool_thread_timeout_thrash)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 1,
.auto_increment = 1,
.initial_size = 0,
.max_size = 1,
};
int iteration;
switch (cmd) {
case TEST_INIT:
info->name = "thread_timeout_thrash";
info->category = "/main/threadpool/";
info->summary = "Thrash threadpool thread timeout";
info->description =
"Repeatedly queue a task when a threadpool thread should timeout.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
ast_threadpool_set_size(pool, 1);
for (iteration = 0; iteration < 30; ++iteration) {
struct simple_task_data *std = NULL;
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + options.idle_timeout,
.tv_nsec = start.tv_usec * 1000
};
std = simple_task_data_alloc();
if (!std) {
goto end;
}
/* Wait until the threadpool thread should timeout due to being idle */
ast_mutex_lock(&tld->lock);
while (ast_cond_timedwait(&tld->cond, &tld->lock, &end) != ETIMEDOUT) {
/* This purposely left empty as we want to loop waiting for a time out */
}
ast_mutex_unlock(&tld->lock);
ast_threadpool_push(pool, simple_task, std);
}
res = wait_until_thread_state(test, tld, 0, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 1, 30, 0, 0, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(tld);
return res;
}
AST_TEST_DEFINE(threadpool_one_task_one_thread)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct simple_task_data *std = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "one_task_one_thread";
info->category = "/main/threadpool/";
info->summary = "Test a single task with a single thread";
info->description =
"Push a task into an empty threadpool, then add a thread to the pool.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
std = simple_task_data_alloc();
if (!std) {
goto end;
}
ast_threadpool_push(pool, simple_task, std);
ast_threadpool_set_size(pool, 1);
/* Threads added to the pool are active when they start,
* so the newly-created thread should immediately execute
* the waiting task.
*/
res = wait_for_completion(test, std);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_empty_notice(test, tld);
if (res == AST_TEST_FAIL) {
goto end;
}
/* After completing the task, the thread should go idle */
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 1, 1, 0, 1, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(std);
return res;
}
AST_TEST_DEFINE(threadpool_one_thread_one_task)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct simple_task_data *std = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "one_thread_one_task";
info->category = "/main/threadpool/";
info->summary = "Test a single thread with a single task";
info->description =
"Add a thread to the pool and then push a task to it.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
std = simple_task_data_alloc();
if (!std) {
goto end;
}
ast_threadpool_set_size(pool, 1);
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
ast_threadpool_push(pool, simple_task, std);
res = wait_for_completion(test, std);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_empty_notice(test, tld);
if (res == AST_TEST_FAIL) {
goto end;
}
/* After completing the task, the thread should go idle */
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 1, 1, 0, 1, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(std);
return res;
}
AST_TEST_DEFINE(threadpool_one_thread_multiple_tasks)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct simple_task_data *std1 = NULL;
struct simple_task_data *std2 = NULL;
struct simple_task_data *std3 = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "one_thread_multiple_tasks";
info->category = "/main/threadpool/";
info->summary = "Test a single thread with multiple tasks";
info->description =
"Add a thread to the pool and then push three tasks to it.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
std1 = simple_task_data_alloc();
std2 = simple_task_data_alloc();
std3 = simple_task_data_alloc();
if (!std1 || !std2 || !std3) {
goto end;
}
ast_threadpool_set_size(pool, 1);
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
ast_threadpool_push(pool, simple_task, std1);
ast_threadpool_push(pool, simple_task, std2);
ast_threadpool_push(pool, simple_task, std3);
res = wait_for_completion(test, std1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_completion(test, std2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_completion(test, std3);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_empty_notice(test, tld);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 0, 3, 0, 1, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(std1);
ast_free(std2);
ast_free(std3);
}
AST_TEST_DEFINE(threadpool_auto_increment)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct simple_task_data *std1 = NULL;
struct simple_task_data *std2 = NULL;
struct simple_task_data *std3 = NULL;
struct simple_task_data *std4 = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.auto_increment = 3,
.initial_size = 0,
};
switch (cmd) {
case TEST_INIT:
info->name = "auto_increment";
info->category = "/main/threadpool/";
info->summary = "Test that the threadpool grows as tasks are added";
info->description =
"Create an empty threadpool and push a task to it. Once the task is\n"
"pushed, the threadpool should add three threads and be able to\n"
"handle the task. The threads should then go idle";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
std1 = simple_task_data_alloc();
std2 = simple_task_data_alloc();
std3 = simple_task_data_alloc();
std4 = simple_task_data_alloc();
if (!std1 || !std2 || !std3 || !std4) {
ast_threadpool_push(pool, simple_task, std1);
/* Pushing the task should result in the threadpool growing
* by three threads. This will allow the task to actually execute
*/
res = wait_for_completion(test, std1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_empty_notice(test, tld);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 3);
if (res == AST_TEST_FAIL) {
goto end;
}
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
/* Now push three tasks into the pool and ensure the pool does not
* grow.
*/
ast_threadpool_push(pool, simple_task, std2);
ast_threadpool_push(pool, simple_task, std3);
ast_threadpool_push(pool, simple_task, std4);
res = wait_for_completion(test, std2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_completion(test, std3);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_completion(test, std4);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_empty_notice(test, tld);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 3);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 0, 4, 0, 3, 1);
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(std1);
ast_free(std2);
ast_free(std3);
ast_free(std4);
ast_free(tld);
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
AST_TEST_DEFINE(threadpool_max_size)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct simple_task_data *std = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.auto_increment = 3,
.initial_size = 0,
.max_size = 2,
};
switch (cmd) {
case TEST_INIT:
info->name = "max_size";
info->category = "/main/threadpool/";
info->summary = "Test that the threadpool does not exceed its maximum size restriction";
info->description =
"Create an empty threadpool and push a task to it. Once the task is\n"
"pushed, the threadpool should attempt to grow by three threads, but the\n"
"pool's restrictions should only allow two threads to be added.";
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
return AST_TEST_FAIL;
}
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
std = simple_task_data_alloc();
if (!std) {
goto end;
}
ast_threadpool_push(pool, simple_task, std);
res = wait_for_completion(test, std);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 1, 1, 0, 2, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(std);
ast_free(tld);
return res;
}
AST_TEST_DEFINE(threadpool_reactivation)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct simple_task_data *std1 = NULL;
struct simple_task_data *std2 = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "reactivation";
info->category = "/main/threadpool/";
info->summary = "Test that a threadpool reactivates when work is added";
info->description =
"Push a task into a threadpool. Make sure the task executes and the\n"
"thread goes idle. Then push a second task and ensure that the thread\n"
"awakens and executes the second task.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
std1 = simple_task_data_alloc();
std2 = simple_task_data_alloc();
if (!std1 || !std2) {
goto end;
}
ast_threadpool_push(pool, simple_task, std1);
ast_threadpool_set_size(pool, 1);
res = wait_for_completion(test, std1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_empty_notice(test, tld);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 1, 1, 0, 1, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
/* Now make sure the threadpool reactivates when we add a second task */
ast_threadpool_push(pool, simple_task, std2);
res = wait_for_completion(test, std2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_empty_notice(test, tld);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 1, 2, 0, 1, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(std1);
ast_free(std2);
ast_free(tld);
int task_executed;
int continue_task;
ast_mutex_t lock;
ast_cond_t stall_cond;
};
static struct complex_task_data *complex_task_data_alloc(void)
{
struct complex_task_data *ctd = ast_calloc(1, sizeof(*ctd));
if (!ctd) {
return NULL;
}
ast_mutex_init(&ctd->lock);
ast_cond_init(&ctd->stall_cond, NULL);
ast_cond_init(&ctd->notify_cond, NULL);
return ctd;
}
static int complex_task(void *data)
{
struct complex_task_data *ctd = data;
SCOPED_MUTEX(lock, &ctd->lock);
/* Notify that we started */
ctd->task_started = 1;
ast_cond_signal(&ctd->notify_cond);
while (!ctd->continue_task) {
ast_cond_wait(&ctd->stall_cond, lock);
}
/* We got poked. Finish up */
ctd->task_executed = 1;
ast_cond_signal(&ctd->notify_cond);
return 0;
}
static void poke_worker(struct complex_task_data *ctd)
{
SCOPED_MUTEX(lock, &ctd->lock);
ctd->continue_task = 1;
ast_cond_signal(&ctd->stall_cond);
}
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
static int wait_for_complex_start(struct complex_task_data *ctd)
{
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + 5,
.tv_nsec = start.tv_usec * 1000
};
SCOPED_MUTEX(lock, &ctd->lock);
while (!ctd->task_started) {
if (ast_cond_timedwait(&ctd->notify_cond, lock, &end) == ETIMEDOUT) {
break;
}
}
return ctd->task_started;
}
static int has_complex_started(struct complex_task_data *ctd)
{
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + 1,
.tv_nsec = start.tv_usec * 1000
};
SCOPED_MUTEX(lock, &ctd->lock);
while (!ctd->task_started) {
if (ast_cond_timedwait(&ctd->notify_cond, lock, &end) == ETIMEDOUT) {
break;
}
}
return ctd->task_started;
}
static enum ast_test_result_state wait_for_complex_completion(struct complex_task_data *ctd)
{
struct timeval start = ast_tvnow();
struct timespec end = {
.tv_sec = start.tv_sec + 5,
.tv_nsec = start.tv_usec * 1000
};
enum ast_test_result_state res = AST_TEST_PASS;
SCOPED_MUTEX(lock, &ctd->lock);
while (!ctd->task_executed) {
if (ast_cond_timedwait(&ctd->notify_cond, lock, &end) == ETIMEDOUT) {
break;
}
}
if (!ctd->task_executed) {
res = AST_TEST_FAIL;
}
return res;
}
AST_TEST_DEFINE(threadpool_task_distribution)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct complex_task_data *ctd1 = NULL;
struct complex_task_data *ctd2 = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "task_distribution";
info->category = "/main/threadpool/";
info->summary = "Test that tasks are evenly distributed to threads";
info->description =
"Push two tasks into a threadpool. Ensure that each is handled by\n"
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
ctd1 = complex_task_data_alloc();
ctd2 = complex_task_data_alloc();
if (!ctd1 || !ctd2) {
goto end;
}
ast_threadpool_push(pool, complex_task, ctd1);
ast_threadpool_push(pool, complex_task, ctd2);
ast_threadpool_set_size(pool, 2);
res = wait_until_thread_state(test, tld, 2, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 0, 2, 2, 0, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
/* The tasks are stalled until we poke them */
poke_worker(ctd1);
poke_worker(ctd2);
res = wait_for_complex_completion(ctd1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_complex_completion(ctd2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 0, 2, 0, 2, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(ctd1);
ast_free(ctd2);
ast_free(tld);
AST_TEST_DEFINE(threadpool_more_destruction)
{
struct ast_threadpool *pool = NULL;
struct ast_threadpool_listener *listener = NULL;
struct complex_task_data *ctd1 = NULL;
struct complex_task_data *ctd2 = NULL;
enum ast_test_result_state res = AST_TEST_FAIL;
struct test_listener_data *tld = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.initial_size = 0,
switch (cmd) {
case TEST_INIT:
Mark Michelson
committed
info->name = "more_destruction";
info->category = "/main/threadpool/";
info->summary = "Test that threads are destroyed as expected";
info->description =
"Push two tasks into a threadpool. Set the threadpool size to 4\n"
"Ensure that there are 2 active and 2 idle threads. Then shrink the\n"
"threadpool down to 1 thread. Ensure that the thread leftover is active\n"
"and ensure that both tasks complete.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
tld = test_alloc();
if (!tld) {
listener = ast_threadpool_listener_alloc(&test_callbacks, tld);
if (!listener) {
goto end;
}
pool = ast_threadpool_create(info->name, listener, &options);
if (!pool) {
goto end;
}
ctd1 = complex_task_data_alloc();
ctd2 = complex_task_data_alloc();
if (!ctd1 || !ctd2) {
goto end;
}
ast_threadpool_push(pool, complex_task, ctd1);
ast_threadpool_push(pool, complex_task, ctd2);
ast_threadpool_set_size(pool, 4);
res = wait_until_thread_state(test, tld, 2, 2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 0, 2, 2, 2, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
ast_threadpool_set_size(pool, 1);
/* Shrinking the threadpool should kill off the two idle threads
* and one of the active threads.
*/
res = wait_until_thread_state(test, tld, 1, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 0, 2, 1, 0, 0);
if (res == AST_TEST_FAIL) {
goto end;
}
/* The tasks are stalled until we poke them */
poke_worker(ctd1);
poke_worker(ctd2);
res = wait_for_complex_completion(ctd1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_for_complex_completion(ctd2);
if (res == AST_TEST_FAIL) {
goto end;
}
res = wait_until_thread_state(test, tld, 0, 1);
if (res == AST_TEST_FAIL) {
goto end;
}
res = listener_check(test, listener, 1, 0, 2, 0, 1, 1);
end:
ast_threadpool_shutdown(pool);
ao2_cleanup(listener);
ast_free(ctd1);
ast_free(ctd2);
ast_free(tld);
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
AST_TEST_DEFINE(threadpool_serializer)
{
int started = 0;
int finished = 0;
enum ast_test_result_state res = AST_TEST_FAIL;
struct ast_threadpool *pool = NULL;
struct ast_taskprocessor *uut = NULL;
struct complex_task_data *data1 = NULL;
struct complex_task_data *data2 = NULL;
struct complex_task_data *data3 = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.auto_increment = 0,
.initial_size = 2,
.max_size = 0,
};
switch (cmd) {
case TEST_INIT:
info->name = "threadpool_serializer";
info->category = "/main/threadpool/";
info->summary = "Test that serializers";
info->description =
"Ensures that tasks enqueued to a serialize execute in sequence.";
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
pool = ast_threadpool_create("threadpool_serializer", NULL, &options);
if (!pool) {
ast_test_status_update(test, "Could not create threadpool\n");
goto end;
}
uut = ast_threadpool_serializer("ser1", pool);
data1 = complex_task_data_alloc();
data2 = complex_task_data_alloc();
data3 = complex_task_data_alloc();
if (!uut || !data1 || !data2 || !data3) {
ast_test_status_update(test, "Allocation failed\n");
goto end;
}
/* This should start right away */
if (ast_taskprocessor_push(uut, complex_task, data1)) {
ast_test_status_update(test, "Failed to enqueue data1\n");
goto end;
}
started = wait_for_complex_start(data1);
if (!started) {
ast_test_status_update(test, "Failed to start data1\n");
goto end;
}
/* This should not start until data 1 is complete */
if (ast_taskprocessor_push(uut, complex_task, data2)) {
ast_test_status_update(test, "Failed to enqueue data2\n");
goto end;
}
started = has_complex_started(data2);
if (started) {
ast_test_status_update(test, "data2 started out of order\n");
goto end;
}
/* But the free thread in the pool can still run */
if (ast_threadpool_push(pool, complex_task, data3)) {
ast_test_status_update(test, "Failed to enqueue data3\n");
}
started = wait_for_complex_start(data3);
if (!started) {
ast_test_status_update(test, "Failed to start data3\n");
goto end;
}
/* Finishing data1 should allow data2 to start */
poke_worker(data1);
finished = wait_for_complex_completion(data1) == AST_TEST_PASS;
if (!finished) {
ast_test_status_update(test, "data1 couldn't finish\n");
goto end;
}
started = wait_for_complex_start(data2);
if (!started) {
ast_test_status_update(test, "Failed to start data2\n");
goto end;
}
/* Finish up */
poke_worker(data2);
finished = wait_for_complex_completion(data2) == AST_TEST_PASS;
if (!finished) {
ast_test_status_update(test, "data2 couldn't finish\n");
goto end;
}
poke_worker(data3);
finished = wait_for_complex_completion(data3) == AST_TEST_PASS;
if (!finished) {
ast_test_status_update(test, "data3 couldn't finish\n");
goto end;
}
res = AST_TEST_PASS;
end:
poke_worker(data1);
poke_worker(data2);
poke_worker(data3);
ast_taskprocessor_unreference(uut);
ast_threadpool_shutdown(pool);
ast_free(data1);
ast_free(data2);
ast_free(data3);
return res;
}
AST_TEST_DEFINE(threadpool_serializer_dupe)
{
enum ast_test_result_state res = AST_TEST_FAIL;
struct ast_threadpool *pool = NULL;
struct ast_taskprocessor *uut = NULL;
struct ast_taskprocessor *there_can_be_only_one = NULL;
struct ast_threadpool_options options = {
.version = AST_THREADPOOL_OPTIONS_VERSION,
.idle_timeout = 0,
.auto_increment = 0,
.initial_size = 2,
.max_size = 0,
};
switch (cmd) {
case TEST_INIT:
info->name = "threadpool_serializer_dupe";
info->category = "/main/threadpool/";
info->summary = "Test that serializers are uniquely named";
info->description =
"Creating two serializers with the same name should\n"
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
pool = ast_threadpool_create("threadpool_serializer", NULL, &options);
if (!pool) {
ast_test_status_update(test, "Could not create threadpool\n");
goto end;
}
uut = ast_threadpool_serializer("highlander", pool);
if (!uut) {
ast_test_status_update(test, "Allocation failed\n");
goto end;
}
there_can_be_only_one = ast_threadpool_serializer("highlander", pool);
if (there_can_be_only_one) {
ast_taskprocessor_unreference(there_can_be_only_one);
ast_test_status_update(test, "Duplicate name error\n");
goto end;
}
res = AST_TEST_PASS;
end:
ast_taskprocessor_unreference(uut);
ast_threadpool_shutdown(pool);
return res;
}
static int unload_module(void)
{
ast_test_unregister(threadpool_push);
ast_test_unregister(threadpool_initial_threads);
ast_test_unregister(threadpool_thread_creation);
ast_test_unregister(threadpool_thread_destruction);
ast_test_unregister(threadpool_thread_timeout);
ast_test_unregister(threadpool_thread_timeout_thrash);
ast_test_unregister(threadpool_one_task_one_thread);
ast_test_unregister(threadpool_one_thread_one_task);
ast_test_unregister(threadpool_one_thread_multiple_tasks);
ast_test_unregister(threadpool_auto_increment);
ast_test_unregister(threadpool_max_size);
ast_test_unregister(threadpool_reactivation);
ast_test_unregister(threadpool_task_distribution);
ast_test_unregister(threadpool_more_destruction);
ast_test_unregister(threadpool_serializer);
ast_test_unregister(threadpool_serializer_dupe);
return 0;
}
static int load_module(void)
{
ast_test_register(threadpool_push);
ast_test_register(threadpool_initial_threads);
ast_test_register(threadpool_thread_creation);
ast_test_register(threadpool_thread_destruction);
ast_test_register(threadpool_thread_timeout);
ast_test_register(threadpool_thread_timeout_thrash);
ast_test_register(threadpool_one_task_one_thread);
ast_test_register(threadpool_one_thread_one_task);
ast_test_register(threadpool_one_thread_multiple_tasks);
ast_test_register(threadpool_auto_increment);
ast_test_register(threadpool_max_size);
ast_test_register(threadpool_reactivation);
ast_test_register(threadpool_task_distribution);
ast_test_register(threadpool_more_destruction);
ast_test_register(threadpool_serializer);
ast_test_register(threadpool_serializer_dupe);
return AST_MODULE_LOAD_SUCCESS;
}
AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "threadpool test module");