diff --git a/res/res_sorcery_memory_cache.c b/res/res_sorcery_memory_cache.c index a37ddfd3dd..d2c648cffa 100644 --- a/res/res_sorcery_memory_cache.c +++ b/res/res_sorcery_memory_cache.c @@ -38,6 +38,73 @@ ASTERISK_REGISTER_FILE() #include "asterisk/sched.h" #include "asterisk/test.h" #include "asterisk/heap.h" +#include "asterisk/cli.h" +#include "asterisk/manager.h" + +/*** DOCUMENTATION + + + Expire (remove) an object from a sorcery memory cache. + + + + + The name of the cache to expire the object from. + + + The name of the object to expire. + + + + Expires (removes) an object from a sorcery memory cache. + + + + + Expire (remove) ALL objects from a sorcery memory cache. + + + + + The name of the cache to expire all objects from. + + + + Expires (removes) ALL objects from a sorcery memory cache. + + + + + Mark an object in a sorcery memory cache as stale. + + + + + The name of the cache to mark the object as stale in. + + + The name of the object to mark as stale. + + + + Marks an object as stale within a sorcery memory cache. + + + + + Marks ALL objects in a sorcery memory cache as stale. + + + + + The name of the cache to mark all object as stale in. + + + + Marks ALL objects in a sorcery memory cache as stale. + + + ***/ /*! \brief Structure for storing a memory cache */ struct sorcery_memory_cache { @@ -403,6 +470,94 @@ static int expire_objects_from_cache(const void *data) return 0; } +/*! + * \internal + * \brief Remove all objects from the cache. + * + * This removes ALL objects from both the hash table and heap. + * + * \pre cache->objects is write-locked + * + * \param cache The cache to empty. + */ +static void remove_all_from_cache(struct sorcery_memory_cache *cache) +{ + while (ast_heap_pop(cache->object_heap)); + + ao2_callback(cache->objects, OBJ_UNLINK | OBJ_NOLOCK | OBJ_NODATA | OBJ_MULTIPLE, + NULL, NULL); + + AST_SCHED_DEL_UNREF(sched, cache->expire_id, ao2_ref(cache, -1)); +} + +/*! + * \internal + * \brief AO2 callback function for making an object stale immediately + * + * This changes the creation time of an object so it appears as though it is stale immediately. + * + * \param obj The cached object + * \param arg The cache itself + * \param flags Unused flags + */ +static int object_stale_callback(void *obj, void *arg, int flags) +{ + struct sorcery_memory_cached_object *cached = obj; + struct sorcery_memory_cache *cache = arg; + + /* Since our granularity is seconds it's possible for something to retrieve us within a window + * where we wouldn't be treated as stale. To ensure that doesn't happen we use the configured stale + * time plus a second. + */ + cached->created = ast_tvsub(cached->created, ast_samp2tv(cache->object_lifetime_stale + 1, 1)); + + return CMP_MATCH; +} + +/*! + * \internal + * \brief Mark an object as stale explicitly. + * + * This changes the creation time of an object so it appears as though it is stale immediately. + * + * \pre cache->objects is read-locked + * + * \param cache The cache the object is in + * \param id The unique identifier of the object + * + * \retval 0 success + * \retval -1 failure + */ +static int mark_object_as_stale_in_cache(struct sorcery_memory_cache *cache, const char *id) +{ + struct sorcery_memory_cached_object *cached; + + cached = ao2_find(cache->objects, id, OBJ_SEARCH_KEY | OBJ_NOLOCK); + if (!cached) { + return -1; + } + + object_stale_callback(cached, cache, 0); + ao2_ref(cached, -1); + + return 0; +} + +/*! + * \internal + * \brief Mark all objects as stale within a cache. + * + * This changes the creation time of ALL objects so they appear as though they are stale. + * + * \pre cache->objects is read-locked + * + * \param cache + */ +static void mark_all_as_stale_in_cache(struct sorcery_memory_cache *cache) +{ + ao2_callback(cache->objects, OBJ_NOLOCK | OBJ_NODATA | OBJ_MULTIPLE, object_stale_callback, cache); +} + /*! * \internal * \brief Schedule a callback for cached object expiration. @@ -900,6 +1055,480 @@ static void sorcery_memory_cache_close(void *data) ao2_ref(cache, -1); } +/*! + * \internal + * \brief CLI tab completion for cache names + */ +static char *sorcery_memory_cache_complete_name(const char *word, int state) +{ + struct sorcery_memory_cache *cache; + struct ao2_iterator it_caches; + int wordlen = strlen(word); + int which = 0; + char *result = NULL; + + it_caches = ao2_iterator_init(caches, 0); + while ((cache = ao2_iterator_next(&it_caches))) { + if (!strncasecmp(word, cache->name, wordlen) + && ++which > state) { + result = ast_strdup(cache->name); + } + ao2_ref(cache, -1); + if (result) { + break; + } + } + ao2_iterator_destroy(&it_caches); + return result; +} + +/*! + * \internal + * \brief CLI command implementation for 'sorcery memory cache show' + */ +static char *sorcery_memory_cache_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct sorcery_memory_cache *cache; + + switch (cmd) { + case CLI_INIT: + e->command = "sorcery memory cache show"; + e->usage = + "Usage: sorcery memory cache show \n" + " Show sorcery memory cache configuration and statistics.\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 4) { + return sorcery_memory_cache_complete_name(a->word, a->n); + } else { + return NULL; + } + } + + if (a->argc != 5) { + return CLI_SHOWUSAGE; + } + + cache = ao2_find(caches, a->argv[4], OBJ_SEARCH_KEY); + if (!cache) { + ast_cli(a->fd, "Specified sorcery memory cache '%s' does not exist\n", a->argv[4]); + return CLI_FAILURE; + } + + ast_cli(a->fd, "Sorcery memory cache: %s\n", cache->name); + ast_cli(a->fd, "Number of objects within cache: %d\n", ao2_container_count(cache->objects)); + if (cache->maximum_objects) { + ast_cli(a->fd, "Maximum allowed objects: %d\n", cache->maximum_objects); + } else { + ast_cli(a->fd, "There is no limit on the maximum number of objects in the cache\n"); + } + if (cache->object_lifetime_maximum) { + ast_cli(a->fd, "Number of seconds before object expires: %d\n", cache->object_lifetime_maximum); + } else { + ast_cli(a->fd, "Object expiration is not enabled - cached objects will not expire\n"); + } + if (cache->object_lifetime_stale) { + ast_cli(a->fd, "Number of seconds before object becomes stale: %d\n", cache->object_lifetime_stale); + } else { + ast_cli(a->fd, "Object staleness is not enabled - cached objects will not go stale\n"); + } + ast_cli(a->fd, "Prefetch: %s\n", AST_CLI_ONOFF(cache->prefetch)); + ast_cli(a->fd, "Expire all objects on reload: %s\n", AST_CLI_ONOFF(cache->expire_on_reload)); + + ao2_ref(cache, -1); + + return CLI_SUCCESS; +} + +/*! \brief Structure used to pass data for printing cached object information */ +struct print_object_details { + /*! \brief The sorcery memory cache */ + struct sorcery_memory_cache *cache; + /*! \brief The CLI arguments */ + struct ast_cli_args *a; +}; + +/*! + * \internal + * \brief Callback function for displaying object within the cache + */ +static int sorcery_memory_cache_print_object(void *obj, void *arg, int flags) +{ +#define FORMAT "%-25.25s %-15u %-15u \n" + struct sorcery_memory_cached_object *cached = obj; + struct print_object_details *details = arg; + int seconds_until_expire = 0, seconds_until_stale = 0; + + if (details->cache->object_lifetime_maximum) { + seconds_until_expire = ast_tvdiff_ms(ast_tvadd(cached->created, ast_samp2tv(details->cache->object_lifetime_maximum, 1)), ast_tvnow()) / 1000; + } + if (details->cache->object_lifetime_stale) { + seconds_until_stale = ast_tvdiff_ms(ast_tvadd(cached->created, ast_samp2tv(details->cache->object_lifetime_stale, 1)), ast_tvnow()) / 1000; + } + + ast_cli(details->a->fd, FORMAT, ast_sorcery_object_get_id(cached->object), MAX(seconds_until_stale, 0), MAX(seconds_until_expire, 0)); + + return CMP_MATCH; +#undef FORMAT +} + +/*! + * \internal + * \brief CLI command implementation for 'sorcery memory cache dump' + */ +static char *sorcery_memory_cache_dump(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ +#define FORMAT "%-25.25s %-15.15s %-15.15s \n" + struct sorcery_memory_cache *cache; + struct print_object_details details; + + switch (cmd) { + case CLI_INIT: + e->command = "sorcery memory cache dump"; + e->usage = + "Usage: sorcery memory cache dump \n" + " Dump a list of the objects within the cache, listed by object identifier.\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 4) { + return sorcery_memory_cache_complete_name(a->word, a->n); + } else { + return NULL; + } + } + + if (a->argc != 5) { + return CLI_SHOWUSAGE; + } + + cache = ao2_find(caches, a->argv[4], OBJ_SEARCH_KEY); + if (!cache) { + ast_cli(a->fd, "Specified sorcery memory cache '%s' does not exist\n", a->argv[4]); + return CLI_FAILURE; + } + + details.cache = cache; + details.a = a; + + ast_cli(a->fd, "Dumping sorcery memory cache '%s':\n", cache->name); + if (!cache->object_lifetime_stale) { + ast_cli(a->fd, " * Staleness is not enabled - objects will not go stale\n"); + } + if (!cache->object_lifetime_maximum) { + ast_cli(a->fd, " * Object lifetime is not enabled - objects will not expire\n"); + } + ast_cli(a->fd, FORMAT, "Object Name", "Stale In", "Expires In"); + ast_cli(a->fd, FORMAT, "-------------------------", "---------------", "---------------"); + ao2_callback(cache->objects, OBJ_NODATA | OBJ_MULTIPLE, sorcery_memory_cache_print_object, &details); + ast_cli(a->fd, FORMAT, "-------------------------", "---------------", "---------------"); + ast_cli(a->fd, "Total number of objects cached: %d\n", ao2_container_count(cache->objects)); + + ao2_ref(cache, -1); + + return CLI_SUCCESS; +#undef FORMAT +} + +/*! + * \internal + * \brief CLI tab completion for cached object names + */ +static char *sorcery_memory_cache_complete_object_name(const char *cache_name, const char *word, int state) +{ + struct sorcery_memory_cache *cache; + struct sorcery_memory_cached_object *cached; + struct ao2_iterator it_cached; + int wordlen = strlen(word); + int which = 0; + char *result = NULL; + + cache = ao2_find(caches, cache_name, OBJ_SEARCH_KEY); + if (!cache) { + return NULL; + } + + it_cached = ao2_iterator_init(cache->objects, 0); + while ((cached = ao2_iterator_next(&it_cached))) { + if (!strncasecmp(word, ast_sorcery_object_get_id(cached->object), wordlen) + && ++which > state) { + result = ast_strdup(ast_sorcery_object_get_id(cached->object)); + } + ao2_ref(cached, -1); + if (result) { + break; + } + } + ao2_iterator_destroy(&it_cached); + + ao2_ref(cache, -1); + + return result; +} + +/*! + * \internal + * \brief CLI command implementation for 'sorcery memory cache expire' + */ +static char *sorcery_memory_cache_expire(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct sorcery_memory_cache *cache; + + switch (cmd) { + case CLI_INIT: + e->command = "sorcery memory cache expire"; + e->usage = + "Usage: sorcery memory cache expire [object name]\n" + " Expire a specific object or ALL objects within a sorcery memory cache.\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 4) { + return sorcery_memory_cache_complete_name(a->word, a->n); + } else if (a->pos == 5) { + return sorcery_memory_cache_complete_object_name(a->argv[4], a->word, a->n); + } else { + return NULL; + } + } + + if (a->argc > 6) { + return CLI_SHOWUSAGE; + } + + cache = ao2_find(caches, a->argv[4], OBJ_SEARCH_KEY); + if (!cache) { + ast_cli(a->fd, "Specified sorcery memory cache '%s' does not exist\n", a->argv[4]); + return CLI_FAILURE; + } + + ao2_wrlock(cache->objects); + if (a->argc == 5) { + remove_all_from_cache(cache); + ast_cli(a->fd, "All objects have been removed from cache '%s'\n", a->argv[4]); + } else { + if (!remove_from_cache(cache, a->argv[5], 1)) { + ast_cli(a->fd, "Successfully expired object '%s' from cache '%s'\n", a->argv[5], a->argv[4]); + } else { + ast_cli(a->fd, "Object '%s' was not expired from cache '%s' as it was not found\n", a->argv[5], + a->argv[4]); + } + } + ao2_unlock(cache->objects); + + ao2_ref(cache, -1); + + return CLI_SUCCESS; +} + +/*! + * \internal + * \brief CLI command implementation for 'sorcery memory cache stale' + */ +static char *sorcery_memory_cache_stale(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct sorcery_memory_cache *cache; + + switch (cmd) { + case CLI_INIT: + e->command = "sorcery memory cache stale"; + e->usage = + "Usage: sorcery memory cache stale [object name]\n" + " Mark a specific object or ALL objects as stale in a sorcery memory cache.\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 4) { + return sorcery_memory_cache_complete_name(a->word, a->n); + } else if (a->pos == 5) { + return sorcery_memory_cache_complete_object_name(a->argv[4], a->word, a->n); + } else { + return NULL; + } + } + + if (a->argc > 6) { + return CLI_SHOWUSAGE; + } + + cache = ao2_find(caches, a->argv[4], OBJ_SEARCH_KEY); + if (!cache) { + ast_cli(a->fd, "Specified sorcery memory cache '%s' does not exist\n", a->argv[4]); + return CLI_FAILURE; + } + + if (!cache->object_lifetime_stale) { + ast_cli(a->fd, "Specified sorcery memory cache '%s' does not have staleness enabled\n", a->argv[4]); + ao2_ref(cache, -1); + return CLI_FAILURE; + } + + ao2_rdlock(cache->objects); + if (a->argc == 5) { + mark_all_as_stale_in_cache(cache); + ast_cli(a->fd, "Marked all objects in sorcery memory cache '%s' as stale\n", a->argv[4]); + } else { + if (!mark_object_as_stale_in_cache(cache, a->argv[5])) { + ast_cli(a->fd, "Successfully marked object '%s' in memory cache '%s' as stale\n", + a->argv[5], a->argv[4]); + } else { + ast_cli(a->fd, "Object '%s' in sorcery memory cache '%s' could not be marked as stale as it was not found\n", + a->argv[5], a->argv[4]); + } + } + ao2_unlock(cache->objects); + + ao2_ref(cache, -1); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_memory_cache[] = { + AST_CLI_DEFINE(sorcery_memory_cache_show, "Show sorcery memory cache information"), + AST_CLI_DEFINE(sorcery_memory_cache_dump, "Dump all objects within a sorcery memory cache"), + AST_CLI_DEFINE(sorcery_memory_cache_expire, "Expire a specific object or ALL objects within a sorcery memory cache"), + AST_CLI_DEFINE(sorcery_memory_cache_stale, "Mark a specific object or ALL objects as stale within a sorcery memory cache"), +}; + +/*! + * \internal + * \brief AMI command implementation for 'SorceryMemoryCacheExpireObject' + */ +static int sorcery_memory_cache_ami_expire_object(struct mansession *s, const struct message *m) +{ + const char *cache_name = astman_get_header(m, "Cache"); + const char *object_name = astman_get_header(m, "Object"); + struct sorcery_memory_cache *cache; + int res; + + if (ast_strlen_zero(cache_name)) { + astman_send_error(s, m, "SorceryMemoryCacheExpireObject requires that a cache name be provided.\n"); + return 0; + } else if (ast_strlen_zero(object_name)) { + astman_send_error(s, m, "SorceryMemoryCacheExpireObject requires that an object name be provided\n"); + return 0; + } + + cache = ao2_find(caches, cache_name, OBJ_SEARCH_KEY); + if (!cache) { + astman_send_error(s, m, "The provided cache does not exist\n"); + return 0; + } + + ao2_wrlock(cache->objects); + res = remove_from_cache(cache, object_name, 1); + ao2_unlock(cache->objects); + + ao2_ref(cache, -1); + + if (!res) { + astman_send_ack(s, m, "The provided object was expired from the cache\n"); + } else { + astman_send_error(s, m, "The provided object could not be expired from the cache\n"); + } + + return 0; +} + +/*! + * \internal + * \brief AMI command implementation for 'SorceryMemoryCacheExpire' + */ +static int sorcery_memory_cache_ami_expire(struct mansession *s, const struct message *m) +{ + const char *cache_name = astman_get_header(m, "Cache"); + struct sorcery_memory_cache *cache; + + if (ast_strlen_zero(cache_name)) { + astman_send_error(s, m, "SorceryMemoryCacheExpire requires that a cache name be provided.\n"); + return 0; + } + + cache = ao2_find(caches, cache_name, OBJ_SEARCH_KEY); + if (!cache) { + astman_send_error(s, m, "The provided cache does not exist\n"); + return 0; + } + + ao2_wrlock(cache->objects); + remove_all_from_cache(cache); + ao2_unlock(cache->objects); + + ao2_ref(cache, -1); + + astman_send_ack(s, m, "All objects were expired from the cache\n"); + + return 0; +} + +/*! + * \internal + * \brief AMI command implementation for 'SorceryMemoryCacheStaleObject' + */ +static int sorcery_memory_cache_ami_stale_object(struct mansession *s, const struct message *m) +{ + const char *cache_name = astman_get_header(m, "Cache"); + const char *object_name = astman_get_header(m, "Object"); + struct sorcery_memory_cache *cache; + int res; + + if (ast_strlen_zero(cache_name)) { + astman_send_error(s, m, "SorceryMemoryCacheStaleObject requires that a cache name be provided.\n"); + return 0; + } else if (ast_strlen_zero(object_name)) { + astman_send_error(s, m, "SorceryMemoryCacheStaleObject requires that an object name be provided\n"); + return 0; + } + + cache = ao2_find(caches, cache_name, OBJ_SEARCH_KEY); + if (!cache) { + astman_send_error(s, m, "The provided cache does not exist\n"); + return 0; + } + + ao2_rdlock(cache->objects); + res = mark_object_as_stale_in_cache(cache, object_name); + ao2_unlock(cache->objects); + + ao2_ref(cache, -1); + + if (!res) { + astman_send_ack(s, m, "The provided object was marked as stale in the cache\n"); + } else { + astman_send_error(s, m, "The provided object could not be marked as stale in the cache\n"); + } + + return 0; +} + +/*! + * \internal + * \brief AMI command implementation for 'SorceryMemoryCacheStale' + */ +static int sorcery_memory_cache_ami_stale(struct mansession *s, const struct message *m) +{ + const char *cache_name = astman_get_header(m, "Cache"); + struct sorcery_memory_cache *cache; + + if (ast_strlen_zero(cache_name)) { + astman_send_error(s, m, "SorceryMemoryCacheStale requires that a cache name be provided.\n"); + return 0; + } + + cache = ao2_find(caches, cache_name, OBJ_SEARCH_KEY); + if (!cache) { + astman_send_error(s, m, "The provided cache does not exist\n"); + return 0; + } + + ao2_rdlock(cache->objects); + mark_all_as_stale_in_cache(cache); + ao2_unlock(cache->objects); + + ao2_ref(cache, -1); + + astman_send_ack(s, m, "All objects were marked as stale in the cache\n"); + + return 0; +} + #ifdef TEST_FRAMEWORK /*! \brief Dummy sorcery object */ @@ -1846,6 +2475,13 @@ static int unload_module(void) ast_sorcery_wizard_unregister(&memory_cache_object_wizard); + ast_cli_unregister_multiple(cli_memory_cache, ARRAY_LEN(cli_memory_cache)); + + ast_manager_unregister("SorceryMemoryCacheExpireObject"); + ast_manager_unregister("SorceryMemoryCacheExpire"); + ast_manager_unregister("SorceryMemoryCacheStaleObject"); + ast_manager_unregister("SorceryMemoryCacheStale"); + AST_TEST_UNREGISTER(open_with_valid_options); AST_TEST_UNREGISTER(open_with_invalid_options); AST_TEST_UNREGISTER(create_and_retrieve); @@ -1860,6 +2496,8 @@ static int unload_module(void) static int load_module(void) { + int res; + sched = ast_sched_context_create(); if (!sched) { ast_log(LOG_ERROR, "Failed to create scheduler for cache management\n"); @@ -1886,6 +2524,17 @@ static int load_module(void) return AST_MODULE_LOAD_DECLINE; } + res = ast_cli_register_multiple(cli_memory_cache, ARRAY_LEN(cli_memory_cache)); + res |= ast_manager_register_xml("SorceryMemoryCacheExpireObject", EVENT_FLAG_SYSTEM, sorcery_memory_cache_ami_expire_object); + res |= ast_manager_register_xml("SorceryMemoryCacheExpire", EVENT_FLAG_SYSTEM, sorcery_memory_cache_ami_expire); + res |= ast_manager_register_xml("SorceryMemoryCacheStaleObject", EVENT_FLAG_SYSTEM, sorcery_memory_cache_ami_stale_object); + res |= ast_manager_register_xml("SorceryMemoryCacheStale", EVENT_FLAG_SYSTEM, sorcery_memory_cache_ami_stale); + + if (res) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + AST_TEST_REGISTER(open_with_valid_options); AST_TEST_REGISTER(open_with_invalid_options); AST_TEST_REGISTER(create_and_retrieve);