res_sorcery_memory_cache: Add basic module implementation.
authorJoshua Colp <jcolp@digium.com>
Sat, 16 May 2015 22:02:50 +0000 (19:02 -0300)
committerJoshua Colp <jcolp@digium.com>
Fri, 22 May 2015 14:28:24 +0000 (09:28 -0500)
This change adds a basic res_sorcery_memory_cache module which implements
configuration option parsing, configuration file parsing for threading,
sorcery interface implementation, and unit tests.

Objects can be added, updated, deleted, and retrieved from the memory
cache. Automatic expiration and stale handling will be added in the
future.

Note that unit tests exist within the module itself in case the
threading done as a result of expiration results in asynchronous
actions (which it likely will). Providing access and a notification
mechanism for an external test module would be complicated and
not worth it.

ASTERISK-25067 #close
Reported by: Matt Jordan

Change-Id: Id8a6a357ef5a83d466f81eee56a67d13eeb118b9

include/asterisk/sorcery.h
main/sorcery.c
res/res_sorcery_memory_cache.c [new file with mode: 0644]

index 9d39ce4..d2dc701 100644 (file)
@@ -1279,6 +1279,15 @@ struct ast_sorcery_object_type *ast_sorcery_get_object_type(const struct ast_sor
 int ast_sorcery_is_object_field_registered(const struct ast_sorcery_object_type *object_type,
                const char *field_name);
 
+/*!
+ * \brief Get the module that has opened the provided sorcery instance.
+ *
+ * \param sorcery The sorcery instance
+ *
+ * \return The module
+ */
+const char *ast_sorcery_get_module(const struct ast_sorcery *sorcery);
+
 #if defined(__cplusplus) || defined(c_plusplus)
 }
 #endif
index c779548..732fb62 100644 (file)
@@ -2328,3 +2328,8 @@ int ast_sorcery_is_object_field_registered(const struct ast_sorcery_object_type
        ao2_cleanup(object_field);
        return res;
 }
+
+const char *ast_sorcery_get_module(const struct ast_sorcery *sorcery)
+{
+       return sorcery->module_name;
+}
\ No newline at end of file
diff --git a/res/res_sorcery_memory_cache.c b/res/res_sorcery_memory_cache.c
new file mode 100644 (file)
index 0000000..1290bd9
--- /dev/null
@@ -0,0 +1,1016 @@
+/*
+ * 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 Memory Cache Object Wizard
+ *
+ * \author Joshua Colp <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_REGISTER_FILE()
+
+#include "asterisk/module.h"
+#include "asterisk/sorcery.h"
+#include "asterisk/astobj2.h"
+#include "asterisk/sched.h"
+#include "asterisk/test.h"
+
+/*! \brief Structure for storing a memory cache */
+struct sorcery_memory_cache {
+       /*! \brief The name of the memory cache */
+       char *name;
+       /*! \brief Objects in the cache */
+       struct ao2_container *objects;
+       /*! \brief The maximum number of objects permitted in the cache, 0 if no limit */
+       unsigned int maximum_objects;
+       /*! \brief The maximum time (in seconds) an object will stay in the cache, 0 if no limit */
+       unsigned int object_lifetime_maximum;
+       /*! \brief The amount of time (in seconds) before an object is marked as stale, 0 if disabled */
+       unsigned int object_lifetime_stale;
+       /*! \brief Whether objects are prefetched from normal storage at load time, 0 if disabled */
+       unsigned int prefetch;
+       /** \brief Whether all objects are expired when the object type is reloaded, 0 if disabled */
+       unsigned int expire_on_reload;
+};
+
+/*! \brief Structure for stored a cached object */
+struct sorcery_memory_cached_object {
+       /*! \brief The cached object */
+       void *object;
+       /*! \brief The time at which the object was created */
+       struct timeval created;
+};
+
+static void *sorcery_memory_cache_open(const char *data);
+static int sorcery_memory_cache_create(const struct ast_sorcery *sorcery, void *data, void *object);
+static void sorcery_memory_cache_load(void *data, const struct ast_sorcery *sorcery, const char *type);
+static void sorcery_memory_cache_reload(void *data, const struct ast_sorcery *sorcery, const char *type);
+static void *sorcery_memory_cache_retrieve_id(const struct ast_sorcery *sorcery, void *data, const char *type,
+       const char *id);
+static int sorcery_memory_cache_delete(const struct ast_sorcery *sorcery, void *data, void *object);
+static void sorcery_memory_cache_close(void *data);
+
+static struct ast_sorcery_wizard memory_cache_object_wizard = {
+       .name = "memory_cache",
+       .open = sorcery_memory_cache_open,
+       .create = sorcery_memory_cache_create,
+       .update = sorcery_memory_cache_create,
+       .delete = sorcery_memory_cache_delete,
+       .load = sorcery_memory_cache_load,
+       .reload = sorcery_memory_cache_reload,
+       .retrieve_id = sorcery_memory_cache_retrieve_id,
+       .close = sorcery_memory_cache_close,
+};
+
+/*! \brief The bucket size for the container of caches */
+#define CACHES_CONTAINER_BUCKET_SIZE 53
+
+/*! \brief The default bucket size for the container of objects in the cache */
+#define CACHE_CONTAINER_BUCKET_SIZE 53
+
+/*! \brief Container of created caches */
+static struct ao2_container *caches;
+
+/*! \brief Scheduler for cache management */
+static struct ast_sched_context *sched;
+
+/*!
+ * \internal
+ * \brief Hashing function for the container holding caches
+ *
+ * \param obj A sorcery memory cache or name of one
+ * \param flags Hashing flags
+ *
+ * \return The hash of the memory cache name
+ */
+static int sorcery_memory_cache_hash(const void *obj, int flags)
+{
+       const struct sorcery_memory_cache *cache = obj;
+       const char *name = obj;
+       int hash;
+
+       switch (flags & (OBJ_SEARCH_OBJECT | OBJ_SEARCH_KEY | OBJ_SEARCH_PARTIAL_KEY)) {
+       default:
+       case OBJ_SEARCH_OBJECT:
+               name = cache->name;
+               /* Fall through */
+       case OBJ_SEARCH_KEY:
+               hash = ast_str_hash(name);
+               break;
+       case OBJ_SEARCH_PARTIAL_KEY:
+               /* Should never happen in hash callback. */
+               ast_assert(0);
+               hash = 0;
+               break;
+       }
+       return hash;
+}
+
+/*!
+ * \internal
+ * \brief Comparison function for the container holding caches
+ *
+ * \param obj A sorcery memory cache
+ * \param arg A sorcery memory cache, or name of one
+ * \param flags Comparison flags
+ *
+ * \retval CMP_MATCH if the name is the same
+ * \retval 0 if the name does not match
+ */
+static int sorcery_memory_cache_cmp(void *obj, void *arg, int flags)
+{
+       const struct sorcery_memory_cache *left = obj;
+       const struct sorcery_memory_cache *right = arg;
+       const char *right_name = arg;
+       int cmp;
+
+       switch (flags & (OBJ_SEARCH_OBJECT | OBJ_SEARCH_KEY | OBJ_SEARCH_PARTIAL_KEY)) {
+       default:
+       case OBJ_SEARCH_OBJECT:
+               right_name = right->name;
+               /* Fall through */
+       case OBJ_SEARCH_KEY:
+               cmp = strcmp(left->name, right_name);
+               break;
+       case OBJ_SEARCH_PARTIAL_KEY:
+               cmp = strncmp(left->name, right_name, strlen(right_name));
+               break;
+       }
+       return cmp ? 0 : CMP_MATCH;
+}
+
+/*!
+ * \internal
+ * \brief Hashing function for the container holding cached objects
+ *
+ * \param obj A cached object or id of one
+ * \param flags Hashing flags
+ *
+ * \return The hash of the cached object id
+ */
+static int sorcery_memory_cached_object_hash(const void *obj, int flags)
+{
+       const struct sorcery_memory_cached_object *cached = obj;
+       const char *name = obj;
+       int hash;
+
+       switch (flags & (OBJ_SEARCH_OBJECT | OBJ_SEARCH_KEY | OBJ_SEARCH_PARTIAL_KEY)) {
+       default:
+       case OBJ_SEARCH_OBJECT:
+               name = ast_sorcery_object_get_id(cached->object);
+               /* Fall through */
+       case OBJ_SEARCH_KEY:
+               hash = ast_str_hash(name);
+               break;
+       case OBJ_SEARCH_PARTIAL_KEY:
+               /* Should never happen in hash callback. */
+               ast_assert(0);
+               hash = 0;
+               break;
+       }
+       return hash;
+}
+
+/*!
+ * \internal
+ * \brief Comparison function for the container holding cached objects
+ *
+ * \param obj A cached object
+ * \param arg A cached object, or id of one
+ * \param flags Comparison flags
+ *
+ * \retval CMP_MATCH if the id is the same
+ * \retval 0 if the id does not match
+ */
+static int sorcery_memory_cached_object_cmp(void *obj, void *arg, int flags)
+{
+       struct sorcery_memory_cached_object *left = obj;
+       struct sorcery_memory_cached_object *right = arg;
+       const char *right_name = arg;
+       int cmp;
+
+       switch (flags & (OBJ_SEARCH_OBJECT | OBJ_SEARCH_KEY | OBJ_SEARCH_PARTIAL_KEY)) {
+       default:
+       case OBJ_SEARCH_OBJECT:
+               right_name = ast_sorcery_object_get_id(right->object);
+               /* Fall through */
+       case OBJ_SEARCH_KEY:
+               cmp = strcmp(ast_sorcery_object_get_id(left->object), right_name);
+               break;
+       case OBJ_SEARCH_PARTIAL_KEY:
+               cmp = strncmp(ast_sorcery_object_get_id(left->object), right_name, strlen(right_name));
+               break;
+       }
+       return cmp ? 0 : CMP_MATCH;
+}
+
+/*!
+ * \internal
+ * \brief Destructor function for a sorcery memory cache
+ *
+ * \param obj A sorcery memory cache
+ */
+static void sorcery_memory_cache_destructor(void *obj)
+{
+       struct sorcery_memory_cache *cache = obj;
+
+       ast_free(cache->name);
+       ao2_cleanup(cache->objects);
+}
+
+/*!
+ * \internal
+ * \brief Destructor function for sorcery memory cached objects
+ *
+ * \param obj A sorcery memory cached object
+ */
+static void sorcery_memory_cached_object_destructor(void *obj)
+{
+       struct sorcery_memory_cached_object *cached = obj;
+
+       ao2_cleanup(cached->object);
+}
+
+/*!
+ * \internal
+ * \brief Callback function to cache an object in a memory cache
+ *
+ * \param sorcery The sorcery instance
+ * \param data The sorcery memory cache
+ * \param object The object to cache
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+static int sorcery_memory_cache_create(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       struct sorcery_memory_cache *cache = data;
+       struct sorcery_memory_cached_object *cached;
+
+       cached = ao2_alloc_options(sizeof(*cached), sorcery_memory_cached_object_destructor,
+               AO2_ALLOC_OPT_LOCK_NOLOCK);
+       if (!cached) {
+               return -1;
+       }
+       cached->object = ao2_bump(object);
+       cached->created = ast_tvnow();
+
+       /* As there is no guarantee that this won't be called by multiple threads wanting to cache
+        * the same object we remove any old ones, which turns this into a create/update function
+        * in reality. As well since there's no guarantee that the object in the cache is the same
+        * one here we remove any old objects using the object identifier.
+        */
+
+       ao2_wrlock(cache->objects);
+       ao2_find(cache->objects, ast_sorcery_object_get_id(object), OBJ_SEARCH_KEY | OBJ_NODATA | OBJ_UNLINK | OBJ_NOLOCK);
+       ao2_link_flags(cache->objects, cached, OBJ_NOLOCK);
+       ao2_unlock(cache->objects);
+
+       ao2_ref(cached, -1);
+
+       return 0;
+}
+
+/*!
+ * \internal
+ * \brief Callback function to retrieve an object from a memory cache
+ *
+ * \param sorcery The sorcery instance
+ * \param data The sorcery memory cache
+ * \param type The type of the object to retrieve
+ * \param id The id of the object to retrieve
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ */
+static void *sorcery_memory_cache_retrieve_id(const struct ast_sorcery *sorcery, void *data, const char *type, const char *id)
+{
+       struct sorcery_memory_cache *cache = data;
+       struct sorcery_memory_cached_object *cached;
+       void *object;
+
+       cached = ao2_find(cache->objects, id, OBJ_SEARCH_KEY);
+       if (!cached) {
+               return NULL;
+       }
+
+       object = ao2_bump(cached->object);
+       ao2_ref(cached, -1);
+
+       return object;
+}
+
+/*!
+ * \internal
+ * \brief Callback function to finish configuring the memory cache and to prefetch objects
+ *
+ * \param data The sorcery memory cache
+ * \param sorcery The sorcery instance
+ * \param type The type of object being loaded
+ */
+static void sorcery_memory_cache_load(void *data, const struct ast_sorcery *sorcery, const char *type)
+{
+       struct sorcery_memory_cache *cache = data;
+
+       /* If no name was explicitly specified generate one given the sorcery instance and object type */
+       if (ast_strlen_zero(cache->name)) {
+               ast_asprintf(&cache->name, "%s/%s", ast_sorcery_get_module(sorcery), type);
+       }
+
+       ao2_link(caches, cache);
+       ast_debug(1, "Memory cache '%s' associated with sorcery instance '%p' of module '%s' with object type '%s'\n",
+               cache->name, sorcery, ast_sorcery_get_module(sorcery), type);
+}
+
+/*!
+ * \internal
+ * \brief Callback function to expire objects from the memory cache on reload (if configured)
+ *
+ * \param data The sorcery memory cache
+ * \param sorcery The sorcery instance
+ * \param type The type of object being reloaded
+ */
+static void sorcery_memory_cache_reload(void *data, const struct ast_sorcery *sorcery, const char *type)
+{
+}
+
+/*!
+ * \internal
+ * \brief Function used to take an unsigned integer based configuration option and parse it
+ *
+ * \param value The string value of the configuration option
+ * \param result The unsigned integer to place the result in
+ *
+ * \retval 0 failure
+ * \retval 1 success
+ */
+static int configuration_parse_unsigned_integer(const char *value, unsigned int *result)
+{
+       if (ast_strlen_zero(value) || !strncmp(value, "-", 1)) {
+               return 0;
+       }
+
+       return sscanf(value, "%30u", result);
+}
+
+/*!
+ * \internal
+ * \brief Callback function to create a new sorcery memory cache using provided configuration
+ *
+ * \param data A stringified configuration for the memory cache
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ */
+static void *sorcery_memory_cache_open(const char *data)
+{
+       char *options = ast_strdup(data), *option;
+       RAII_VAR(struct sorcery_memory_cache *, cache, NULL, ao2_cleanup);
+
+       cache = ao2_alloc_options(sizeof(*cache), sorcery_memory_cache_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK);
+       if (!cache) {
+               return NULL;
+       }
+
+       /* If no configuration options have been provided this memory cache will operate in a default
+        * configuration.
+        */
+       while (!ast_strlen_zero(options) && (option = strsep(&options, ","))) {
+               char *name = strsep(&option, "="), *value = option;
+
+               if (!strcasecmp(name, "name")) {
+                       if (ast_strlen_zero(value)) {
+                               ast_log(LOG_ERROR, "A name must be specified for the memory cache\n");
+                               return NULL;
+                       }
+                       ast_free(cache->name);
+                       cache->name = ast_strdup(value);
+               } else if (!strcasecmp(name, "maximum_objects")) {
+                       if (configuration_parse_unsigned_integer(value, &cache->maximum_objects) != 1) {
+                               ast_log(LOG_ERROR, "Unsupported maximum objects value of '%s' used for memory cache\n",
+                                       value);
+                               return NULL;
+                       }
+               } else if (!strcasecmp(name, "object_lifetime_maximum")) {
+                       if (configuration_parse_unsigned_integer(value, &cache->object_lifetime_maximum) != 1) {
+                               ast_log(LOG_ERROR, "Unsupported object maximum lifetime value of '%s' used for memory cache\n",
+                                       value);
+                               return NULL;
+                       }
+               } else if (!strcasecmp(name, "object_lifetime_stale")) {
+                       if (configuration_parse_unsigned_integer(value, &cache->object_lifetime_stale) != 1) {
+                               ast_log(LOG_ERROR, "Unsupported object stale lifetime value of '%s' used for memory cache\n",
+                                       value);
+                               return NULL;
+                       }
+               } else if (!strcasecmp(name, "prefetch")) {
+                       cache->prefetch = ast_true(value);
+               } else if (!strcasecmp(name, "expire_on_reload")) {
+                       cache->expire_on_reload = ast_true(value);
+               } else {
+                       ast_log(LOG_ERROR, "Unsupported option '%s' used for memory cache\n", name);
+                       return NULL;
+               }
+       }
+
+       cache->objects = ao2_container_alloc_options(AO2_ALLOC_OPT_LOCK_RWLOCK,
+               cache->maximum_objects ? cache->maximum_objects : CACHE_CONTAINER_BUCKET_SIZE,
+               sorcery_memory_cached_object_hash, sorcery_memory_cached_object_cmp);
+       if (!cache->objects) {
+               ast_log(LOG_ERROR, "Could not create a container to hold cached objects for memory cache\n");
+               return NULL;
+       }
+
+       /* The memory cache is not linked to the caches container until the load callback is invoked.
+        * Linking occurs there so an intelligent cache name can be constructed using the module of
+        * the sorcery instance and the specific object type if no cache name was specified as part
+        * of the configuration.
+        */
+
+       /* This is done as RAII_VAR will drop the reference */
+       return ao2_bump(cache);
+}
+
+/*!
+ * \internal
+ * \brief Callback function to delete an object from a memory cache
+ *
+ * \param sorcery The sorcery instance
+ * \param data The sorcery memory cache
+ * \param object The object to cache
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+static int sorcery_memory_cache_delete(const struct ast_sorcery *sorcery, void *data, void *object)
+{
+       struct sorcery_memory_cache *cache = data;
+
+       /* There is no guarantee that the object we have cached is the one we will be provided
+        * with in this callback function. As a result of this we remove the cached object based on
+        * the identifier and not the object itself.
+        */
+       ao2_find(cache->objects, ast_sorcery_object_get_id(object), OBJ_SEARCH_KEY | OBJ_NODATA | OBJ_UNLINK);
+
+       return 0;
+}
+
+/*!
+ * \internal
+ * \brief Callback function to terminate a memory cache
+ *
+ * \param data The sorcery memory cache
+ */
+static void sorcery_memory_cache_close(void *data)
+{
+       struct sorcery_memory_cache *cache = data;
+
+       /* This can occur if a cache is created but never loaded */
+       if (!ast_strlen_zero(cache->name)) {
+               ao2_unlink(caches, cache);
+       }
+
+       ao2_ref(cache, -1);
+}
+
+#ifdef TEST_FRAMEWORK
+
+/*! \brief Dummy sorcery object */
+struct test_sorcery_object {
+       SORCERY_OBJECT(details);
+};
+
+/*!
+ * \internal
+ * \brief Allocator for test object
+ *
+ * \param id The identifier for the object
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ */
+static void *test_sorcery_object_alloc(const char *id)
+{
+       return ast_sorcery_generic_alloc(sizeof(struct test_sorcery_object), NULL);
+}
+
+/*!
+ * \internal
+ * \brief Allocator for test sorcery instance
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ */
+static struct ast_sorcery *alloc_and_initialize_sorcery(void)
+{
+       struct ast_sorcery *sorcery;
+
+       if (!(sorcery = ast_sorcery_open())) {
+               return NULL;
+       }
+
+       if ((ast_sorcery_apply_default(sorcery, "test", "memory", NULL) != AST_SORCERY_APPLY_SUCCESS) ||
+               ast_sorcery_internal_object_register(sorcery, "test", test_sorcery_object_alloc, NULL, NULL)) {
+               ast_sorcery_unref(sorcery);
+               return NULL;
+       }
+
+       return sorcery;
+}
+
+AST_TEST_DEFINE(open_with_valid_options)
+{
+       int res = AST_TEST_PASS;
+       struct sorcery_memory_cache *cache;
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "open_with_valid_options";
+               info->category = "/res/res_sorcery_memory_cache/";
+               info->summary = "Attempt to create sorcery memory caches using valid options";
+               info->description = "This test performs the following:\n"
+                       "\t* Creates a memory cache with default configuration\n"
+                       "\t* Creates a memory cache with a maximum object count of 10 and verifies it\n"
+                       "\t* Creates a memory cache with a maximum object lifetime of 60 and verifies it\n"
+                       "\t* Creates a memory cache with a stale object lifetime of 90 and verifies it\n";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       cache = sorcery_memory_cache_open("");
+       if (!cache) {
+               ast_test_status_update(test, "Failed to create a sorcery memory cache using default configuration\n");
+               res = AST_TEST_FAIL;
+       } else {
+               sorcery_memory_cache_close(cache);
+       }
+
+       cache = sorcery_memory_cache_open("maximum_objects=10");
+       if (!cache) {
+               ast_test_status_update(test, "Failed to create a sorcery memory cache with a maximum object count of 10\n");
+               res = AST_TEST_FAIL;
+       } else {
+               if (cache->maximum_objects != 10) {
+                       ast_test_status_update(test, "Created a sorcery memory cache with a maximum object count of 10 but it has '%u'\n",
+                               cache->maximum_objects);
+               }
+               sorcery_memory_cache_close(cache);
+       }
+
+       cache = sorcery_memory_cache_open("object_lifetime_maximum=60");
+       if (!cache) {
+               ast_test_status_update(test, "Failed to create a sorcery memory cache with a maximum object lifetime of 60\n");
+               res = AST_TEST_FAIL;
+       } else {
+               if (cache->object_lifetime_maximum != 60) {
+                       ast_test_status_update(test, "Created a sorcery memory cache with a maximum object lifetime of 60 but it has '%u'\n",
+                               cache->object_lifetime_maximum);
+               }
+               sorcery_memory_cache_close(cache);
+       }
+
+       cache = sorcery_memory_cache_open("object_lifetime_stale=90");
+       if (!cache) {
+               ast_test_status_update(test, "Failed to create a sorcery memory cache with a stale object lifetime of 90\n");
+               res = AST_TEST_FAIL;
+       } else {
+               if (cache->object_lifetime_stale != 90) {
+                       ast_test_status_update(test, "Created a sorcery memory cache with a stale object lifetime of 90 but it has '%u'\n",
+                               cache->object_lifetime_stale);
+               }
+               sorcery_memory_cache_close(cache);
+       }
+
+
+       return res;
+}
+
+AST_TEST_DEFINE(open_with_invalid_options)
+{
+       int res = AST_TEST_PASS;
+       struct sorcery_memory_cache *cache;
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "open_with_invalid_options";
+               info->category = "/res/res_sorcery_memory_cache/";
+               info->summary = "Attempt to create sorcery memory caches using invalid options";
+               info->description = "This test attempts to perform the following:\n"
+                       "\t* Create a memory cache with an empty name\n"
+                       "\t* Create a memory cache with a maximum object count of -1\n"
+                       "\t* Create a memory cache with a maximum object count of toast\n"
+                       "\t* Create a memory cache with a maximum object lifetime of -1\n"
+                       "\t* Create a memory cache with a maximum object lifetime of toast\n"
+                       "\t* Create a memory cache with a stale object lifetime of -1\n"
+                       "\t* Create a memory cache with a stale object lifetime of toast\n";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       cache = sorcery_memory_cache_open("name=");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with an empty name\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       cache = sorcery_memory_cache_open("maximum_objects=-1");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with a maximum object count of -1\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       cache = sorcery_memory_cache_open("maximum_objects=toast");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with a maximum object count of toast\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       cache = sorcery_memory_cache_open("object_lifetime_maximum=-1");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with an object lifetime maximum of -1\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       cache = sorcery_memory_cache_open("object_lifetime_maximum=toast");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with an object lifetime maximum of toast\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       cache = sorcery_memory_cache_open("object_lifetime_stale=-1");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with a stale object lifetime of -1\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       cache = sorcery_memory_cache_open("object_lifetime_stale=toast");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with a stale object lifetime of toast\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       cache = sorcery_memory_cache_open("tacos");
+       if (cache) {
+               ast_test_status_update(test, "Created a sorcery memory cache with an invalid configuration option 'tacos'\n");
+               sorcery_memory_cache_close(cache);
+               res = AST_TEST_FAIL;
+       }
+
+       return res;
+}
+
+AST_TEST_DEFINE(create_and_retrieve)
+{
+       int res = AST_TEST_FAIL;
+       struct ast_sorcery *sorcery = NULL;
+       struct sorcery_memory_cache *cache = NULL;
+       RAII_VAR(void *, object, NULL, ao2_cleanup);
+       RAII_VAR(void *, cached_object, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "create";
+               info->category = "/res/res_sorcery_memory_cache/";
+               info->summary = "Attempt to create an object in the cache";
+               info->description = "This test performs the following:\n"
+                       "\t* Creates a memory cache with default options\n"
+                       "\t* Creates a sorcery instance with a test object\n"
+                       "\t* Creates a test object with an id of test\n"
+                       "\t* Pushes the test object into the memory cache\n"
+                       "\t* Confirms that the test object is in the cache\n";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       cache = sorcery_memory_cache_open("");
+       if (!cache) {
+               ast_test_status_update(test, "Failed to create a sorcery memory cache using default options\n");
+               goto cleanup;
+       }
+
+       if (ao2_container_count(cache->objects)) {
+               ast_test_status_update(test, "Memory cache contains cached objects before we added one\n");
+               goto cleanup;
+       }
+
+       sorcery = alloc_and_initialize_sorcery();
+       if (!sorcery) {
+               ast_test_status_update(test, "Failed to create a test sorcery instance\n");
+               goto cleanup;
+       }
+
+       object = ast_sorcery_alloc(sorcery, "test", "test");
+       if (!object) {
+               ast_test_status_update(test, "Failed to allocate a test object\n");
+               goto cleanup;
+       }
+
+       sorcery_memory_cache_create(sorcery, cache, object);
+
+       if (!ao2_container_count(cache->objects)) {
+               ast_test_status_update(test, "Added test object to memory cache but cache remains empty\n");
+               goto cleanup;
+       }
+
+       cached_object = sorcery_memory_cache_retrieve_id(sorcery, cache, "test", "test");
+       if (!cached_object) {
+               ast_test_status_update(test, "Object placed into memory cache could not be retrieved\n");
+               goto cleanup;
+       }
+
+       if (cached_object != object) {
+               ast_test_status_update(test, "Object retrieved from memory cached is not the one we cached\n");
+               goto cleanup;
+       }
+
+       res = AST_TEST_PASS;
+
+cleanup:
+       if (cache) {
+               sorcery_memory_cache_close(cache);
+       }
+       if (sorcery) {
+               ast_sorcery_unref(sorcery);
+       }
+
+       return res;
+}
+
+AST_TEST_DEFINE(update)
+{
+       int res = AST_TEST_FAIL;
+       struct ast_sorcery *sorcery = NULL;
+       struct sorcery_memory_cache *cache = NULL;
+       RAII_VAR(void *, original_object, NULL, ao2_cleanup);
+       RAII_VAR(void *, updated_object, NULL, ao2_cleanup);
+       RAII_VAR(void *, cached_object, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "create";
+               info->category = "/res/res_sorcery_memory_cache/";
+               info->summary = "Attempt to create and then update an object in the cache";
+               info->description = "This test performs the following:\n"
+                       "\t* Creates a memory cache with default options\n"
+                       "\t* Creates a sorcery instance with a test object\n"
+                       "\t* Creates a test object with an id of test\n"
+                       "\t* Pushes the test object into the memory cache\n"
+                       "\t* Confirms that the test object is in the cache\n"
+                       "\t* Creates a new test object with the same id of test\n"
+                       "\t* Pushes the new test object into the memory cache\n"
+                       "\t* Confirms that the new test object has replaced the old one\n";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       cache = sorcery_memory_cache_open("");
+       if (!cache) {
+               ast_test_status_update(test, "Failed to create a sorcery memory cache using default options\n");
+               goto cleanup;
+       }
+
+       if (ao2_container_count(cache->objects)) {
+               ast_test_status_update(test, "Memory cache contains cached objects before we added one\n");
+               goto cleanup;
+       }
+
+       sorcery = alloc_and_initialize_sorcery();
+       if (!sorcery) {
+               ast_test_status_update(test, "Failed to create a test sorcery instance\n");
+               goto cleanup;
+       }
+
+       original_object = ast_sorcery_alloc(sorcery, "test", "test");
+       if (!original_object) {
+               ast_test_status_update(test, "Failed to allocate a test object\n");
+               goto cleanup;
+       }
+
+       sorcery_memory_cache_create(sorcery, cache, original_object);
+
+       updated_object = ast_sorcery_alloc(sorcery, "test", "test");
+       if (!updated_object) {
+               ast_test_status_update(test, "Failed to allocate an updated test object\n");
+               goto cleanup;
+       }
+
+       sorcery_memory_cache_create(sorcery, cache, updated_object);
+
+       if (ao2_container_count(cache->objects) != 1) {
+               ast_test_status_update(test, "Added updated test object to memory cache but cache now contains %d objects instead of 1\n",
+                       ao2_container_count(cache->objects));
+               goto cleanup;
+       }
+
+       cached_object = sorcery_memory_cache_retrieve_id(sorcery, cache, "test", "test");
+       if (!cached_object) {
+               ast_test_status_update(test, "Updated object placed into memory cache could not be retrieved\n");
+               goto cleanup;
+       }
+
+       if (cached_object == original_object) {
+               ast_test_status_update(test, "Updated object placed into memory cache but old one is being retrieved\n");
+               goto cleanup;
+       } else if (cached_object != updated_object) {
+               ast_test_status_update(test, "Updated object placed into memory cache but different one is being retrieved\n");
+               goto cleanup;
+       }
+
+       res = AST_TEST_PASS;
+
+cleanup:
+       if (cache) {
+               sorcery_memory_cache_close(cache);
+       }
+       if (sorcery) {
+               ast_sorcery_unref(sorcery);
+       }
+
+       return res;
+}
+
+AST_TEST_DEFINE(delete)
+{
+       int res = AST_TEST_FAIL;
+       struct ast_sorcery *sorcery = NULL;
+       struct sorcery_memory_cache *cache = NULL;
+       RAII_VAR(void *, object, NULL, ao2_cleanup);
+       RAII_VAR(void *, cached_object, NULL, ao2_cleanup);
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "delete";
+               info->category = "/res/res_sorcery_memory_cache/";
+               info->summary = "Attempt to create and then delete an object in the cache";
+               info->description = "This test performs the following:\n"
+                       "\t* Creates a memory cache with default options\n"
+                       "\t* Creates a sorcery instance with a test object\n"
+                       "\t* Creates a test object with an id of test\n"
+                       "\t* Pushes the test object into the memory cache\n"
+                       "\t* Confirms that the test object is in the cache\n"
+                       "\t* Deletes the test object from the cache\n"
+                       "\t* Confirms that the test object is no longer in the cache\n";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       cache = sorcery_memory_cache_open("");
+       if (!cache) {
+               ast_test_status_update(test, "Failed to create a sorcery memory cache using default options\n");
+               goto cleanup;
+       }
+
+       if (ao2_container_count(cache->objects)) {
+               ast_test_status_update(test, "Memory cache contains cached objects before we added one\n");
+               goto cleanup;
+       }
+
+       sorcery = alloc_and_initialize_sorcery();
+       if (!sorcery) {
+               ast_test_status_update(test, "Failed to create a test sorcery instance\n");
+               goto cleanup;
+       }
+
+       object = ast_sorcery_alloc(sorcery, "test", "test");
+       if (!object) {
+               ast_test_status_update(test, "Failed to allocate a test object\n");
+               goto cleanup;
+       }
+
+       sorcery_memory_cache_create(sorcery, cache, object);
+
+       if (!ao2_container_count(cache->objects)) {
+               ast_test_status_update(test, "Added test object to memory cache but cache contains no objects\n");
+               goto cleanup;
+       }
+
+       cached_object = sorcery_memory_cache_retrieve_id(sorcery, cache, "test", "test");
+       if (!cached_object) {
+               ast_test_status_update(test, "Test object placed into memory cache could not be retrieved\n");
+               goto cleanup;
+       }
+
+       ao2_ref(cached_object, -1);
+       cached_object = NULL;
+
+       sorcery_memory_cache_delete(sorcery, cache, object);
+
+       cached_object = sorcery_memory_cache_retrieve_id(sorcery, cache, "test", "test");
+       if (cached_object) {
+               ast_test_status_update(test, "Test object deleted from memory cache can still be retrieved\n");
+               goto cleanup;
+       }
+
+       res = AST_TEST_PASS;
+
+cleanup:
+       if (cache) {
+               sorcery_memory_cache_close(cache);
+       }
+       if (sorcery) {
+               ast_sorcery_unref(sorcery);
+       }
+
+       return res;
+}
+
+#endif
+
+static int unload_module(void)
+{
+       if (sched) {
+               ast_sched_context_destroy(sched);
+               sched = NULL;
+       }
+
+       ao2_cleanup(caches);
+
+       ast_sorcery_wizard_unregister(&memory_cache_object_wizard);
+
+       AST_TEST_UNREGISTER(open_with_valid_options);
+       AST_TEST_UNREGISTER(open_with_invalid_options);
+       AST_TEST_UNREGISTER(create_and_retrieve);
+       AST_TEST_UNREGISTER(update);
+       AST_TEST_UNREGISTER(delete);
+
+       return 0;
+}
+
+static int load_module(void)
+{
+       sched = ast_sched_context_create();
+       if (!sched) {
+               ast_log(LOG_ERROR, "Failed to create scheduler for cache management\n");
+               unload_module();
+               return AST_MODULE_LOAD_DECLINE;
+       }
+
+       if (ast_sched_start_thread(sched)) {
+               ast_log(LOG_ERROR, "Failed to create scheduler thread for cache management\n");
+               unload_module();
+               return AST_MODULE_LOAD_DECLINE;
+       }
+
+       caches = ao2_container_alloc(CACHES_CONTAINER_BUCKET_SIZE, sorcery_memory_cache_hash,
+               sorcery_memory_cache_cmp);
+       if (!caches) {
+               ast_log(LOG_ERROR, "Failed to create container for configured caches\n");
+               unload_module();
+               return AST_MODULE_LOAD_DECLINE;
+       }
+
+       if (ast_sorcery_wizard_register(&memory_cache_object_wizard)) {
+               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);
+       AST_TEST_REGISTER(update);
+       AST_TEST_REGISTER(delete);
+
+       return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Sorcery Memory Cache Object Wizard",
+       .support_level = AST_MODULE_SUPPORT_CORE,
+       .load = load_module,
+       .unload = unload_module,
+       .load_pri = AST_MODPRI_REALTIME_DRIVER,
+);