diff --git a/res/res_sorcery_memory_cache.c b/res/res_sorcery_memory_cache.c index 10ef59a35a..26df12a69a 100644 --- a/res/res_sorcery_memory_cache.c +++ b/res/res_sorcery_memory_cache.c @@ -418,6 +418,9 @@ static int remove_from_cache(struct sorcery_memory_cache *cache, const char *id, if (!hash_object) { return -1; } + + ast_assert(!strcmp(ast_sorcery_object_get_id(hash_object->object), id)); + oldest_object = ast_heap_peek(cache->object_heap, 1); heap_object = ast_heap_remove(cache->object_heap, hash_object); @@ -537,6 +540,8 @@ static int mark_object_as_stale_in_cache(struct sorcery_memory_cache *cache, con return -1; } + ast_assert(!strcmp(ast_sorcery_object_get_id(cached->object), id)); + object_stale_callback(cached, cache, 0); ao2_ref(cached, -1); @@ -718,6 +723,7 @@ static int sorcery_memory_cache_create(const struct ast_sorcery *sorcery, void * ao2_unlock(cache->objects); return -1; } + ast_assert(ao2_container_count(cache->objects) != cache->maximum_objects); } if (add_to_cache(cache, cached)) { ast_log(LOG_ERROR, "Unable to add object '%s' to the cache\n", @@ -826,6 +832,8 @@ static void *sorcery_memory_cache_retrieve_id(const struct ast_sorcery *sorcery, return NULL; } + ast_assert(!strcmp(ast_sorcery_object_get_id(cached->object), id)); + if (cache->object_lifetime_stale) { struct timeval elapsed; diff --git a/tests/test_sorcery_memory_cache_thrash.c b/tests/test_sorcery_memory_cache_thrash.c new file mode 100644 index 0000000000..89ce82955a --- /dev/null +++ b/tests/test_sorcery_memory_cache_thrash.c @@ -0,0 +1,620 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * 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 Sorcery Unit Tests + * + * \author Joshua Colp + * + */ + +/*** MODULEINFO + TEST_FRAMEWORK + core + ***/ + +#include "asterisk.h" + +ASTERISK_REGISTER_FILE() + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/sorcery.h" +#include "asterisk/logger.h" +#include "asterisk/vector.h" +#include "asterisk/cli.h" + +/*! \brief The default amount of time (in seconds) that thrash unit tests execute for */ +#define TEST_THRASH_TIME 3 + +/*! \brief The number of threads to use for retrieving for applicable tests */ +#define TEST_THRASH_RETRIEVERS 25 + +/*! \brief The number of threads to use for updating for applicable tests*/ +#define TEST_THRASH_UPDATERS 25 + +/*! \brief Structure for a memory cache thras thread */ +struct sorcery_memory_cache_thrash_thread { + /*! \brief The thread thrashing the cache */ + pthread_t thread; + /*! \brief Sorcery instance being tested */ + struct ast_sorcery *sorcery; + /*! \brief The number of unique objects we should restrict ourself to */ + unsigned int unique_objects; + /*! \brief Set when the thread should stop */ + unsigned int stop; + /*! \brief Average time spent executing sorcery operation in this thread */ + unsigned int average_execution_time; +}; + +/*! \brief Structure for memory cache thrasing */ +struct sorcery_memory_cache_thrash { + /*! \brief The sorcery instance being tested */ + struct ast_sorcery *sorcery; + /*! \brief The number of threads which are updating */ + unsigned int update_threads; + /*! \brief The average execution time of sorcery update operations */ + unsigned int average_update_execution_time; + /*! \brief The number of threads which are retrieving */ + unsigned int retrieve_threads; + /*! \brief The average execution time of sorcery retrieve operations */ + unsigned int average_retrieve_execution_time; + /*! \brief Threads which are updating or reading from the cache */ + AST_VECTOR(, struct sorcery_memory_cache_thrash_thread *) threads; +}; + +/*! + * \brief Sorcery object created based on backend data + */ +struct test_data { + SORCERY_OBJECT(details); +}; + +/*! + * \brief Allocation callback for test_data sorcery object + */ +static void *test_data_alloc(const char *id) +{ + return ast_sorcery_generic_alloc(sizeof(struct test_data), NULL); +} + +/*! + * \brief Callback for retrieving sorcery object by ID + * + * \param sorcery The sorcery instance + * \param data Unused + * \param type The object type. Will always be "test". + * \param id The object id. Will always be "test". + * + * \retval NULL Backend data successfully allocated + * \retval non-NULL Backend data could not be successfully allocated + */ +static void *mock_retrieve_id(const struct ast_sorcery *sorcery, void *data, + const char *type, const char *id) +{ + return ast_sorcery_alloc(sorcery, type, id); +} + +/*! + * \brief Callback for updating a sorcery object + * + * \param sorcery The sorcery instance + * \param data Unused + * \param object The object to update. + * + */ +static int mock_update(const struct ast_sorcery *sorcery, void *data, + void *object) +{ + return 0; +} + +/*! + * \brief A mock sorcery wizard used for the stale test + */ +static struct ast_sorcery_wizard mock_wizard = { + .name = "mock", + .retrieve_id = mock_retrieve_id, + .update = mock_update, +}; + +/*! + * \internal + * \brief Destructor for sorcery memory cache thrasher + * + * \param obj The sorcery memory cache thrash structure + */ +static void sorcery_memory_cache_thrash_destroy(void *obj) +{ + struct sorcery_memory_cache_thrash *thrash = obj; + int idx; + + if (thrash->sorcery) { + ast_sorcery_unref(thrash->sorcery); + } + + for (idx = 0; idx < AST_VECTOR_SIZE(&thrash->threads); ++idx) { + struct sorcery_memory_cache_thrash_thread *thread; + + thread = AST_VECTOR_GET(&thrash->threads, idx); + ast_free(thread); + } + AST_VECTOR_FREE(&thrash->threads); + + ast_sorcery_wizard_unregister(&mock_wizard); +} + +/*! + * \internal + * \brief Set up thrasing against a memory cache on a sorcery instance + * + * \param cache_configuration The sorcery memory cache configuration to use + * \param update_threads The number of threads which should be constantly updating sorcery + * \param retrieve_threads The number of threads which should be constantly retrieving from sorcery + * \param unique_objects The number of unique objects that can exist + * + * \retval non-NULL success + * \retval NULL failure + */ +static struct sorcery_memory_cache_thrash *sorcery_memory_cache_thrash_create(const char *cache_configuration, + unsigned int update_threads, unsigned int retrieve_threads, unsigned int unique_objects) +{ + struct sorcery_memory_cache_thrash *thrash; + struct sorcery_memory_cache_thrash_thread *thread; + unsigned int total_threads = update_threads + retrieve_threads; + + thrash = ao2_alloc_options(sizeof(*thrash), sorcery_memory_cache_thrash_destroy, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!thrash) { + return NULL; + } + + thrash->update_threads = update_threads; + thrash->retrieve_threads = retrieve_threads; + + ast_sorcery_wizard_register(&mock_wizard); + + thrash->sorcery = ast_sorcery_open(); + if (!thrash->sorcery) { + ao2_ref(thrash, -1); + return NULL; + } + + ast_sorcery_apply_wizard_mapping(thrash->sorcery, "test", "memory_cache", + !strcmp(cache_configuration, "default") ? "" : cache_configuration, 1); + ast_sorcery_apply_wizard_mapping(thrash->sorcery, "test", "mock", NULL, 0); + ast_sorcery_internal_object_register(thrash->sorcery, "test", test_data_alloc, NULL, NULL); + + if (AST_VECTOR_INIT(&thrash->threads, update_threads + retrieve_threads)) { + ao2_ref(thrash, -1); + return NULL; + } + + while (AST_VECTOR_SIZE(&thrash->threads) != total_threads) { + thread = ast_calloc(1, sizeof(*thread)); + + if (!thread) { + ao2_ref(thrash, -1); + return NULL; + } + + thread->thread = AST_PTHREADT_NULL; + thread->unique_objects = unique_objects; + + /* This purposely holds no ref as the main thrash structure does */ + thread->sorcery = thrash->sorcery; + + AST_VECTOR_APPEND(&thrash->threads, thread); + } + + return thrash; +} + +/*! + * \internal + * \brief Thrashing cache update thread + * + * \param data The sorcery memory cache thrash thread + */ +static void *sorcery_memory_cache_thrash_update(void *data) +{ + struct sorcery_memory_cache_thrash_thread *thread = data; + struct timeval start; + unsigned int object_id; + char object_id_str[AST_UUID_STR_LEN]; + void *object; + + while (!thread->stop) { + object_id = ast_random() % thread->unique_objects; + snprintf(object_id_str, sizeof(object_id_str), "%u", object_id); + + object = ast_sorcery_alloc(thread->sorcery, "test", object_id_str); + ast_assert(object != NULL); + + start = ast_tvnow(); + ast_sorcery_update(thread->sorcery, object); + thread->average_execution_time = (thread->average_execution_time + ast_tvdiff_ms(ast_tvnow(), start)) / 2; + ao2_ref(object, -1); + } + + return NULL; +} + +/*! + * \internal + * \brief Thrashing cache retrieve thread + * + * \param data The sorcery memory cache thrash thread + */ +static void *sorcery_memory_cache_thrash_retrieve(void *data) +{ + struct sorcery_memory_cache_thrash_thread *thread = data; + struct timeval start; + unsigned int object_id; + char object_id_str[AST_UUID_STR_LEN]; + void *object; + + while (!thread->stop) { + object_id = ast_random() % thread->unique_objects; + snprintf(object_id_str, sizeof(object_id_str), "%u", object_id); + + start = ast_tvnow(); + object = ast_sorcery_retrieve_by_id(thread->sorcery, "test", object_id_str); + thread->average_execution_time = (thread->average_execution_time + ast_tvdiff_ms(ast_tvnow(), start)) / 2; + ast_assert(object != NULL); + + ao2_ref(object, -1); + } + + return NULL; +} + +/*! + * \internal + * \brief Stop thrashing against a sorcery memory cache + * + * \param thrash The sorcery memory cache thrash structure + */ +static void sorcery_memory_cache_thrash_stop(struct sorcery_memory_cache_thrash *thrash) +{ + int idx; + + for (idx = 0; idx < AST_VECTOR_SIZE(&thrash->threads); ++idx) { + struct sorcery_memory_cache_thrash_thread *thread; + + thread = AST_VECTOR_GET(&thrash->threads, idx); + if (thread->thread == AST_PTHREADT_NULL) { + continue; + } + + thread->stop = 1; + + pthread_join(thread->thread, NULL); + + if (idx < thrash->update_threads) { + thrash->average_update_execution_time += thread->average_execution_time; + } else { + thrash->average_retrieve_execution_time += thread->average_execution_time; + } + } + + if (thrash->update_threads) { + thrash->average_update_execution_time /= thrash->update_threads; + } + if (thrash->retrieve_threads) { + thrash->average_retrieve_execution_time /= thrash->retrieve_threads; + } +} + +/*! + * \internal + * \brief Start thrashing against a sorcery memory cache + * + * \param thrash The sorcery memory cache thrash structure + * + * \retval 0 success + * \retval -1 failure + */ +static int sorcery_memory_cache_thrash_start(struct sorcery_memory_cache_thrash *thrash) +{ + int idx; + + for (idx = 0; idx < AST_VECTOR_SIZE(&thrash->threads); ++idx) { + struct sorcery_memory_cache_thrash_thread *thread; + + thread = AST_VECTOR_GET(&thrash->threads, idx); + + if (ast_pthread_create(&thread->thread, NULL, idx < thrash->update_threads ? + sorcery_memory_cache_thrash_update : sorcery_memory_cache_thrash_retrieve, thread)) { + sorcery_memory_cache_thrash_stop(thrash); + return -1; + } + } + + return 0; +} + +/*! + * \internal + * \brief CLI command implementation for 'sorcery memory cache thrash' + */ +static char *sorcery_memory_cache_cli_thrash(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct sorcery_memory_cache_thrash *thrash; + unsigned int thrash_time, unique_objects, retrieve_threads, update_threads; + + switch (cmd) { + case CLI_INIT: + e->command = "sorcery memory cache thrash"; + e->usage = + "Usage: sorcery memory cache thrash \n" + " Create a sorcery instance with a memory cache using the provided configuration and thrash it.\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 9) { + return CLI_SHOWUSAGE; + } + + if (sscanf(a->argv[5], "%30u", &thrash_time) != 1) { + ast_cli(a->fd, "An invalid value of '%s' has been provided for the thrashing time\n", a->argv[5]); + return CLI_FAILURE; + } else if (sscanf(a->argv[6], "%30u", &unique_objects) != 1) { + ast_cli(a->fd, "An invalid value of '%s' has been provided for number of unique objects\n", a->argv[6]); + return CLI_FAILURE; + } else if (sscanf(a->argv[7], "%30u", &retrieve_threads) != 1) { + ast_cli(a->fd, "An invalid value of '%s' has been provided for the number of retrieve threads\n", a->argv[7]); + return CLI_FAILURE; + } else if (sscanf(a->argv[8], "%30u", &update_threads) != 1) { + ast_cli(a->fd, "An invalid value of '%s' has been provided for the number of update threads\n", a->argv[8]); + return CLI_FAILURE; + } + + thrash = sorcery_memory_cache_thrash_create(a->argv[4], update_threads, retrieve_threads, unique_objects); + if (!thrash) { + ast_cli(a->fd, "Could not create a sorcery memory cache thrash test using the provided arguments\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "Starting cache thrash test.\n"); + ast_cli(a->fd, "Memory cache configuration: %s\n", a->argv[4]); + ast_cli(a->fd, "Amount of time to perform test: %u seconds\n", thrash_time); + ast_cli(a->fd, "Number of unique objects: %u\n", unique_objects); + ast_cli(a->fd, "Number of retrieve threads: %u\n", retrieve_threads); + ast_cli(a->fd, "Number of update threads: %u\n", update_threads); + + sorcery_memory_cache_thrash_start(thrash); + while ((thrash_time = sleep(thrash_time))); + sorcery_memory_cache_thrash_stop(thrash); + + ast_cli(a->fd, "Stopped cache thrash test\n"); + + ast_cli(a->fd, "Average retrieve execution time (in milliseconds): %u\n", thrash->average_retrieve_execution_time); + ast_cli(a->fd, "Average update execution time (in milliseconds): %u\n", thrash->average_update_execution_time); + + ao2_ref(thrash, -1); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_memory_cache_thrash[] = { + AST_CLI_DEFINE(sorcery_memory_cache_cli_thrash, "Thrash a sorcery memory cache"), +}; + +/*! + * \internal + * \brief Perform a thrash test against a cache + * + * \param test The unit test being run + * \param cache_configuration The underlying cache configuration + * \param thrash_time How long (in seconds) to thrash the cache for + * \param unique_objects The number of unique objects + * \param retrieve_threads The number of threads constantly doing a retrieve + * \param update_threads The number of threads constantly doing an update + * + * \retval AST_TEST_PASS success + * \retval AST_TEST_FAIL failure + */ +static enum ast_test_result_state nominal_thrash(struct ast_test *test, const char *cache_configuration, + unsigned int thrash_time, unsigned int unique_objects, unsigned int retrieve_threads, + unsigned int update_threads) +{ + struct sorcery_memory_cache_thrash *thrash; + + thrash = sorcery_memory_cache_thrash_create(cache_configuration, update_threads, retrieve_threads, unique_objects); + if (!thrash) { + return AST_TEST_FAIL; + } + + sorcery_memory_cache_thrash_start(thrash); + while ((thrash_time = sleep(thrash_time))); + sorcery_memory_cache_thrash_stop(thrash); + + ao2_ref(thrash, -1); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(low_unique_object_count_immediately_stale) +{ + switch (cmd) { + case TEST_INIT: + info->name = "low_unique_object_count_immediately_stale"; + info->category = "/res/res_sorcery_memory_cache/thrash/"; + info->summary = "Thrash a cache with low number of unique objects that are immediately stale"; + info->description = "This test creates a cache with objects that are stale\n" + "after 1 second. It also creates 25 threads which are constantly attempting\n" + "to retrieve the objects. This test confirms that the background refreshes\n" + "being done as a result of going stale do not conflict or cause problems with\n" + "the large number of retrieve threads.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_thrash(test, "object_lifetime_stale=1", TEST_THRASH_TIME, 10, TEST_THRASH_RETRIEVERS, 0); +} + +AST_TEST_DEFINE(low_unique_object_count_immediately_expire) +{ + switch (cmd) { + case TEST_INIT: + info->name = "low_unique_object_count_immediately_expire"; + info->category = "/res/res_sorcery_memory_cache/thrash/"; + info->summary = "Thrash a cache with low number of unique objects that are immediately expired"; + info->description = "This test creates a cache with objects that are expired\n" + "after 1 second. It also creates 25 threads which are constantly attempting\n" + "to retrieve the objects. This test confirms that the expiration process does\n" + "not cause a problem as the retrieve threads execute.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_thrash(test, "object_lifetime_maximum=1", TEST_THRASH_TIME, 10, TEST_THRASH_RETRIEVERS, 0); +} + +AST_TEST_DEFINE(low_unique_object_count_high_concurrent_updates) +{ + switch (cmd) { + case TEST_INIT: + info->name = "low_unique_object_count_high_concurrent_updates"; + info->category = "/res/res_sorcery_memory_cache/thrash/"; + info->summary = "Thrash a cache with low number of unique objects that are updated frequently"; + info->description = "This test creates a cache with objects that are being constantly\n" + "updated and retrieved at the same time. This will create contention between all\n" + "of the threads as the write lock is held for the updates. This test confirms that\n" + "no problems occur in this situation.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_thrash(test, "default", TEST_THRASH_TIME, 10, TEST_THRASH_RETRIEVERS, TEST_THRASH_UPDATERS); +} + +AST_TEST_DEFINE(unique_objects_exceeding_maximum) +{ + switch (cmd) { + case TEST_INIT: + info->name = "unique_objects_exceeding_maximum"; + info->category = "/res/res_sorcery_memory_cache/thrash/"; + info->summary = "Thrash a cache with a fixed maximum object count"; + info->description = "This test creates a cache with a maximum number of objects\n" + "allowed in it. The maximum number of unique objects, however, far exceeds the\n" + "the maximum number allowed in the cache. This test confirms that the cache does\n" + "not exceed the maximum and that the removal of older objects does not cause\n" + "a problem.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_thrash(test, "maximum_objects=10", TEST_THRASH_TIME, 100, TEST_THRASH_RETRIEVERS, 0); +} + +AST_TEST_DEFINE(unique_objects_exceeding_maximum_with_expire_and_stale) +{ + switch (cmd) { + case TEST_INIT: + info->name = "unique_objects_exceeding_maximum_with_expire_and_stale"; + info->category = "/res/res_sorcery_memory_cache/thrash/"; + info->summary = "Thrash a cache with a fixed maximum object count with objects that expire and go stale"; + info->description = "This test creates a cache with a maximum number of objects\n" + "allowed in it with objects that also go stale after a period of time and expire.\n" + "A number of threads are created that constantly retrieve from the cache, causing\n" + "both stale refresh and expiration to occur. This test confirms that the combination\n" + "of these do not present a problem.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_thrash(test, "maximum_objects=10,object_lifetime_maximum=2,object_lifetime_stale=1", + TEST_THRASH_TIME * 2, 100, TEST_THRASH_RETRIEVERS, 0); +} + +AST_TEST_DEFINE(conflicting_expire_and_stale) +{ + switch (cmd) { + case TEST_INIT: + info->name = "conflicting_expire_and_stale"; + info->category = "/res/res_sorcery_memory_cache/thrash/"; + info->summary = "Thrash a cache with a large number of objects that expire and go stale"; + info->description = "This test creates a cache with a large number of objects that expire\n" + "and go stale. As there is such a large number this ensures that both operations occur.\n" + "This test confirms that stale refreshing and expiration do not conflict.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_thrash(test, "object_lifetime_maximum=2,object_lifetime_stale=1", TEST_THRASH_TIME * 2, 5000, + TEST_THRASH_RETRIEVERS, 0); +} + +AST_TEST_DEFINE(high_object_count_without_expiration) +{ + switch (cmd) { + case TEST_INIT: + info->name = "high_object_count_without_expiration"; + info->category = "/res/res_sorcery_memory_cache/thrash/"; + info->summary = "Thrash a cache with a large number of objects"; + info->description = "This test creates a cache with a large number of objects that persist.\n" + "A large number of threads are created which constantly retrieve from the cache.\n" + "This test confirms that the large number of retrieves do not cause a problem.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_thrash(test, "default", TEST_THRASH_TIME, 5000, TEST_THRASH_RETRIEVERS, 0); +} + +static int unload_module(void) +{ + ast_cli_unregister_multiple(cli_memory_cache_thrash, ARRAY_LEN(cli_memory_cache_thrash)); + AST_TEST_UNREGISTER(low_unique_object_count_immediately_stale); + AST_TEST_UNREGISTER(low_unique_object_count_immediately_expire); + AST_TEST_UNREGISTER(low_unique_object_count_high_concurrent_updates); + AST_TEST_UNREGISTER(unique_objects_exceeding_maximum); + AST_TEST_UNREGISTER(unique_objects_exceeding_maximum_with_expire_and_stale); + AST_TEST_UNREGISTER(conflicting_expire_and_stale); + AST_TEST_UNREGISTER(high_object_count_without_expiration); + + return 0; +} + +static int load_module(void) +{ + ast_cli_register_multiple(cli_memory_cache_thrash, ARRAY_LEN(cli_memory_cache_thrash)); + AST_TEST_REGISTER(low_unique_object_count_immediately_stale); + AST_TEST_REGISTER(low_unique_object_count_immediately_expire); + AST_TEST_REGISTER(low_unique_object_count_high_concurrent_updates); + AST_TEST_REGISTER(unique_objects_exceeding_maximum); + AST_TEST_REGISTER(unique_objects_exceeding_maximum_with_expire_and_stale); + AST_TEST_REGISTER(conflicting_expire_and_stale); + AST_TEST_REGISTER(high_object_count_without_expiration); + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Sorcery Cache Thrasing test module");