res_sorcery_memory_cache: Add CLI commands and AMI actions.
authorJoshua Colp <jcolp@digium.com>
Tue, 26 May 2015 12:34:47 +0000 (09:34 -0300)
committerJoshua Colp <jcolp@digium.com>
Fri, 29 May 2015 17:00:55 +0000 (14:00 -0300)
This change adds the following CLI commands and AMI actions:

sorcery memory cache show
sorcery memory cache dump
sorcery memory cache expire
sorcery memory cache stale

SorceryMemoryCacheExpire
SorceryMemoryCacheExpireObject
SorceryMemoryCacheStale
SorceryMemoryCacheStaleObject

These allow both examination and manipulation of sorcery memory
caches from external sources.

Cached objects can be explicitly expired from a cache or marked
as stale. If expired they are immediately removed. If marked as
stale they will be background refreshed when next retrieved.

ASTERISK-25067
Reported by Matt Jordan

Change-Id: I68e03cfd8c34b5e07f4b6ee4fd93a3f4a00a3d9e

res/res_sorcery_memory_cache.c

index a37ddfd..d2c648c 100644 (file)
@@ -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
+       <manager name="SorceryMemoryCacheExpireObject" language="en_US">
+               <synopsis>
+                       Expire (remove) an object from a sorcery memory cache.
+               </synopsis>
+               <syntax>
+                       <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+                       <parameter name="Cache" required="true">
+                               <para>The name of the cache to expire the object from.</para>
+                       </parameter>
+                       <parameter name="Object" required="true">
+                               <para>The name of the object to expire.</para>
+                       </parameter>
+               </syntax>
+               <description>
+                       <para>Expires (removes) an object from a sorcery memory cache.</para>
+               </description>
+       </manager>
+       <manager name="SorceryMemoryCacheExpire" language="en_US">
+               <synopsis>
+                       Expire (remove) ALL objects from a sorcery memory cache.
+               </synopsis>
+               <syntax>
+                       <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+                       <parameter name="Cache" required="true">
+                               <para>The name of the cache to expire all objects from.</para>
+                       </parameter>
+               </syntax>
+               <description>
+                       <para>Expires (removes) ALL objects from a sorcery memory cache.</para>
+               </description>
+       </manager>
+       <manager name="SorceryMemoryCacheStaleObject" language="en_US">
+               <synopsis>
+                       Mark an object in a sorcery memory cache as stale.
+               </synopsis>
+               <syntax>
+                       <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+                       <parameter name="Cache" required="true">
+                               <para>The name of the cache to mark the object as stale in.</para>
+                       </parameter>
+                       <parameter name="Object" required="true">
+                               <para>The name of the object to mark as stale.</para>
+                       </parameter>
+               </syntax>
+               <description>
+                       <para>Marks an object as stale within a sorcery memory cache.</para>
+               </description>
+       </manager>
+       <manager name="SorceryMemoryCacheStale" language="en_US">
+               <synopsis>
+                       Marks ALL objects in a sorcery memory cache as stale.
+               </synopsis>
+               <syntax>
+                       <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+                       <parameter name="Cache" required="true">
+                               <para>The name of the cache to mark all object as stale in.</para>
+                       </parameter>
+               </syntax>
+               <description>
+                       <para>Marks ALL objects in a sorcery memory cache as stale.</para>
+               </description>
+       </manager>
+ ***/
 
 /*! \brief Structure for storing a memory cache */
 struct sorcery_memory_cache {
@@ -405,6 +472,94 @@ static int expire_objects_from_cache(const void *data)
 
 /*!
  * \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.
  *
  * \pre cache->objects is write-locked
@@ -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 <name>\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 <name>\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 <cache name> [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 <cache name> [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);