2015-06-02 12:20:00 -03:00
/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2015, Digium, Inc.
*
* Joshua Colp <jcolp@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 Sorcery Unit Tests
*
* \author Joshua Colp <jcolp@digium.com>
*
*/
/*** MODULEINFO
<depend>TEST_FRAMEWORK</depend>
<support_level>core</support_level>
***/
#include "asterisk.h"
#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 ;
2017-11-06 18:33:13 -05:00
if ( AST_VECTOR_APPEND ( & thrash -> threads , thread )) {
ast_free ( thread );
ao2_ref ( thrash , - 1 );
return NULL ;
}
2015-06-02 12:20:00 -03:00
}
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 ;
2016-02-09 09:11:36 -04:00
}
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 ;
}
2015-06-02 12:20:00 -03:00
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 <cache configuration> <amount of time to thrash the cache> <number of unique objects> <number of retrieve threads> <number of update threads> \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 "
2015-06-24 16:39:38 -05:00
"the large number of retrieve threads." ;
2015-06-02 12:20:00 -03:00
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 "
2015-06-24 16:39:38 -05:00
"not cause a problem as the retrieve threads execute." ;
2015-06-02 12:20:00 -03:00
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 "
2015-06-24 16:39:38 -05:00
"no problems occur in this situation." ;
2015-06-02 12:20:00 -03:00
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 "
2015-06-24 16:39:38 -05:00
"a problem." ;
2015-06-02 12:20:00 -03:00
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 "
2015-06-24 16:39:38 -05:00
"of these do not present a problem." ;
2015-06-02 12:20:00 -03:00
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 "
2015-06-24 16:39:38 -05:00
"This test confirms that stale refreshing and expiration do not conflict." ;
2015-06-02 12:20:00 -03:00
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 "
2015-06-24 16:39:38 -05:00
"This test confirms that the large number of retrieves do not cause a problem." ;
2015-06-02 12:20:00 -03:00
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" );