Skip to content
Snippets Groups Projects
res_sorcery_memory_cache.c 54.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * 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"
    
    #include "asterisk/heap.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 Heap of cached objects. Oldest object is at the top. */
    	struct ast_heap *object_heap;
    
    	/*! \brief Scheduler item for expiring oldest object. */
    	int expire_id;
    #ifdef TEST_FRAMEWORK
    	/*! \brief Variable used to indicate we should notify a test when we reach empty */
    	unsigned int cache_notify;
    	/*! \brief Mutex lock used for signaling when the cache has reached empty */
    	ast_mutex_t lock;
    	/*! \brief Condition used for signaling when the cache has reached empty */
    	ast_cond_t cond;
    	/*! \brief Variable that is set when the cache has reached empty */
    	unsigned int cache_completed;
    #endif
    
    };
    
    /*! \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;
    
    	/*! \brief index required by heap */
    	ssize_t __heap_index;
    
    	/*! \brief scheduler id of stale update task */
    	int stale_update_sched_id;
    
    };
    
    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 Height of heap for cache object heap. Allows 31 initial objects */
    #define CACHE_HEAP_INIT_HEIGHT 5
    
    
    /*! \brief Container of created caches */
    static struct ao2_container *caches;
    
    /*! \brief Scheduler for cache management */
    static struct ast_sched_context *sched;
    
    
    #define STALE_UPDATE_THREAD_ID 0x5EED1E55
    AST_THREADSTORAGE(stale_update_id_storage);
    
    static int is_stale_update(void)
    {
    	uint32_t *stale_update_thread_id;
    
    	stale_update_thread_id = ast_threadstorage_get(&stale_update_id_storage,
    		sizeof(*stale_update_thread_id));
    	if (!stale_update_thread_id) {
    		return 0;
    	}
    
    	return *stale_update_thread_id == STALE_UPDATE_THREAD_ID;
    }
    
    static void start_stale_update(void)
    {
    	uint32_t *stale_update_thread_id;
    
    	stale_update_thread_id = ast_threadstorage_get(&stale_update_id_storage,
    		sizeof(*stale_update_thread_id));
    	if (!stale_update_thread_id) {
    		ast_log(LOG_ERROR, "Could not set stale update ID for sorcery memory cache thread\n");
    		return;
    	}
    
    	*stale_update_thread_id = STALE_UPDATE_THREAD_ID;
    }
    
    static void end_stale_update(void)
    {
    	uint32_t *stale_update_thread_id;
    
    	stale_update_thread_id = ast_threadstorage_get(&stale_update_id_storage,
    		sizeof(*stale_update_thread_id));
    	if (!stale_update_thread_id) {
    		ast_log(LOG_ERROR, "Could not set stale update ID for sorcery memory cache thread\n");
    		return;
    	}
    
    	*stale_update_thread_id = 0;
    }
    
    
    /*!
     * \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);
    
    	if (cache->object_heap) {
    		ast_heap_destroy(cache->object_heap);
    	}
    
    }
    
    /*!
     * \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);
    }
    
    
    static int schedule_cache_expiration(struct sorcery_memory_cache *cache);
    
    
    /*!
     * \internal
     * \brief Remove an object from the cache.
     *
     * This removes the item from both the hashtable and the heap.
     *
     * \pre cache->objects is write-locked
     *
     * \param cache The cache from which the object is being removed.
     * \param id The sorcery object id of the object to remove.
    
     * \param reschedule Reschedule cache expiration if this was the oldest object.
    
     *
     * \retval 0 Success
     * \retval non-zero Failure
     */
    
    static int remove_from_cache(struct sorcery_memory_cache *cache, const char *id, int reschedule)
    
    {
    	struct sorcery_memory_cached_object *hash_object;
    
    	struct sorcery_memory_cached_object *oldest_object;
    
    	struct sorcery_memory_cached_object *heap_object;
    
    	hash_object = ao2_find(cache->objects, id,
    		OBJ_SEARCH_KEY | OBJ_UNLINK | OBJ_NOLOCK);
    	if (!hash_object) {
    		return -1;
    	}
    
    	oldest_object = ast_heap_peek(cache->object_heap, 1);
    
    	heap_object = ast_heap_remove(cache->object_heap, hash_object);
    
    	ast_assert(heap_object == hash_object);
    
    	ao2_ref(hash_object, -1);
    
    
    	if (reschedule && (oldest_object == heap_object)) {
    		schedule_cache_expiration(cache);
    	}
    
    	return 0;
    }
    
    /*!
     * \internal
     * \brief Scheduler callback invoked to expire old objects
     *
     * \param data The opaque callback data (in our case, the memory cache)
     */
    static int expire_objects_from_cache(const void *data)
    {
    	struct sorcery_memory_cache *cache = (struct sorcery_memory_cache *)data;
    	struct sorcery_memory_cached_object *cached;
    
    	ao2_wrlock(cache->objects);
    
    	cache->expire_id = -1;
    
    	/* This is an optimization for objects which have been cached close to eachother */
    	while ((cached = ast_heap_peek(cache->object_heap, 1))) {
    		int expiration;
    
    		expiration = ast_tvdiff_ms(ast_tvadd(cached->created, ast_samp2tv(cache->object_lifetime_maximum, 1)), ast_tvnow());
    
    		/* If the current oldest object has not yet expired stop and reschedule for it */
    		if (expiration > 0) {
    			break;
    		}
    
    		remove_from_cache(cache, ast_sorcery_object_get_id(cached->object), 0);
    	}
    
    	schedule_cache_expiration(cache);
    
    	ao2_unlock(cache->objects);
    
    	ao2_ref(cache, -1);
    
    	return 0;
    }
    
    /*!
     * \internal
     * \brief Schedule a callback for cached object expiration.
     *
     * \pre cache->objects is write-locked
     *
     * \param cache The cache that is having its callback scheduled.
     *
     * \retval 0 success
     * \retval -1 failure
     */
    static int schedule_cache_expiration(struct sorcery_memory_cache *cache)
    {
    	struct sorcery_memory_cached_object *cached;
    	int expiration = 0;
    
    	if (!cache->object_lifetime_maximum) {
    		return 0;
    	}
    
    	if (cache->expire_id != -1) {
    		/* If we can't unschedule this expiration then it is currently attempting to run,
    		 * so let it run - it just means that it'll be the one scheduling instead of us.
    		 */
    		if (ast_sched_del(sched, cache->expire_id)) {
    			return 0;
    		}
    
    		/* Since it successfully cancelled we need to drop the ref to the cache it had */
    		ao2_ref(cache, -1);
    		cache->expire_id = -1;
    	}
    
    	cached = ast_heap_peek(cache->object_heap, 1);
    	if (!cached) {
    #ifdef TEST_FRAMEWORK
    		ast_mutex_lock(&cache->lock);
    		cache->cache_completed = 1;
    		ast_cond_signal(&cache->cond);
    		ast_mutex_unlock(&cache->lock);
    #endif
    		return 0;
    	}
    
    	expiration = MAX(ast_tvdiff_ms(ast_tvadd(cached->created, ast_samp2tv(cache->object_lifetime_maximum, 1)), ast_tvnow()),
    		1);
    
    	cache->expire_id = ast_sched_add(sched, expiration, expire_objects_from_cache, ao2_bump(cache));
    	if (cache->expire_id < 0) {
    		ao2_ref(cache, -1);
    		return -1;
    	}
    
    
    	return 0;
    }
    
    /*!
     * \internal
     * \brief Remove the oldest item from the cache.
     *
     * \pre cache->objects is write-locked
     *
     * \param cache The cache from which to remove the oldest object
     *
     * \retval 0 Success
     * \retval non-zero Failure
     */
    static int remove_oldest_from_cache(struct sorcery_memory_cache *cache)
    {
    	struct sorcery_memory_cached_object *heap_old_object;
    	struct sorcery_memory_cached_object *hash_old_object;
    
    	heap_old_object = ast_heap_pop(cache->object_heap);
    	if (!heap_old_object) {
    		return -1;
    	}
    	hash_old_object = ao2_find(cache->objects, heap_old_object,
    		OBJ_SEARCH_OBJECT | OBJ_UNLINK | OBJ_NOLOCK);
    
    	ast_assert(heap_old_object == hash_old_object);
    
    	ao2_ref(hash_old_object, -1);
    
    	return 0;
    }
    
    /*!
     * \internal
     * \brief Add a new object to the cache.
     *
     * \pre cache->objects is write-locked
     *
     * \param cache The cache in which to add the new object
     * \param cached_object The object to add to the cache
     *
     * \retval 0 Success
     * \retval non-zero Failure
     */
    static int add_to_cache(struct sorcery_memory_cache *cache,
    		struct sorcery_memory_cached_object *cached_object)
    {
    	if (!ao2_link_flags(cache->objects, cached_object, OBJ_NOLOCK)) {
    		return -1;
    	}
    
    	if (ast_heap_push(cache->object_heap, cached_object)) {
    		ao2_find(cache->objects, cached_object,
    			OBJ_SEARCH_OBJECT | OBJ_UNLINK | OBJ_NODATA | OBJ_NOLOCK);
    		return -1;
    	}
    
    
    	if (cache->expire_id == -1) {
    		schedule_cache_expiration(cache);
    	}
    
    
    /*!
     * \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(sizeof(*cached), sorcery_memory_cached_object_destructor);
    
    	if (!cached) {
    		return -1;
    	}
    	cached->object = ao2_bump(object);
    	cached->created = ast_tvnow();
    
    	cached->stale_update_sched_id = -1;
    
    
    	/* 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);
    
    	remove_from_cache(cache, ast_sorcery_object_get_id(object), 1);
    
    	if (cache->maximum_objects && ao2_container_count(cache->objects) >= cache->maximum_objects) {
    		if (remove_oldest_from_cache(cache)) {
    			ast_log(LOG_ERROR, "Unable to make room in cache for sorcery object '%s'.\n",
    				ast_sorcery_object_get_id(object));
    			ao2_ref(cached, -1);
    			ao2_unlock(cache->objects);
    			return -1;
    		}
    	}
    	if (add_to_cache(cache, cached)) {
    		ast_log(LOG_ERROR, "Unable to add object '%s' to the cache\n",
    			ast_sorcery_object_get_id(object));
    		ao2_ref(cached, -1);
    		ao2_unlock(cache->objects);
    		return -1;
    	}
    
    	ao2_unlock(cache->objects);
    
    	ao2_ref(cached, -1);
    	return 0;
    }
    
    
    struct stale_update_task_data {
    	struct ast_sorcery *sorcery;
    	struct sorcery_memory_cache *cache;
    	void *object;
    };
    
    static void stale_update_task_data_destructor(void *obj)
    {
    	struct stale_update_task_data *task_data = obj;
    
    	ao2_cleanup(task_data->cache);
    	ao2_cleanup(task_data->object);
    	ast_sorcery_unref(task_data->sorcery);
    }
    
    static struct stale_update_task_data *stale_update_task_data_alloc(struct ast_sorcery *sorcery,
    		struct sorcery_memory_cache *cache, const char *type, void *object)
    {
    	struct stale_update_task_data *task_data;
    
    	task_data = ao2_alloc_options(sizeof(*task_data), stale_update_task_data_destructor,
    		AO2_ALLOC_OPT_LOCK_NOLOCK);
    	if (!task_data) {
    		return NULL;
    	}
    
    	task_data->sorcery = ao2_bump(sorcery);
    	task_data->cache = ao2_bump(cache);
    	task_data->object = ao2_bump(object);
    
    	return task_data;
    }
    
    static int stale_item_update(const void *data)
    {
    	struct stale_update_task_data *task_data = (struct stale_update_task_data *) data;
    	void *object;
    
    	start_stale_update();
    
    	object = ast_sorcery_retrieve_by_id(task_data->sorcery,
    		ast_sorcery_object_get_type(task_data->object),
    		ast_sorcery_object_get_id(task_data->object));
    	if (!object) {
    		ast_debug(1, "Backend no longer has object type '%s' ID '%s'. Removing from cache\n",
    			ast_sorcery_object_get_type(task_data->object),
    			ast_sorcery_object_get_id(task_data->object));
    		sorcery_memory_cache_delete(task_data->sorcery, task_data->cache,
    			task_data->object);
    	} else {
    		ast_debug(1, "Refreshing stale cache object type '%s' ID '%s'\n",
    			ast_sorcery_object_get_type(task_data->object),
    			ast_sorcery_object_get_id(task_data->object));
    		sorcery_memory_cache_create(task_data->sorcery, task_data->cache,
    			object);
    	}
    
    	ao2_ref(task_data, -1);
    	end_stale_update();
    
    	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;
    	}
    
    
    	if (cache->object_lifetime_stale) {
    		struct timeval elapsed;
    
    		elapsed = ast_tvsub(ast_tvnow(), cached->created);
    		if (elapsed.tv_sec > cache->object_lifetime_stale) {
    			ao2_lock(cached);
    			if (cached->stale_update_sched_id == -1) {
    				struct stale_update_task_data *task_data;
    
    				task_data = stale_update_task_data_alloc((struct ast_sorcery *)sorcery, cache,
    					type, cached->object);
    				if (task_data) {
    					ast_debug(1, "Cached sorcery object type '%s' ID '%s' is stale. Refreshing\n",
    						type, id);
    					cached->stale_update_sched_id = ast_sched_add(sched, 1, stale_item_update, task_data);
    				} else {
    					ast_log(LOG_ERROR, "Unable to update stale cached object type '%s', ID '%s'.\n",
    						type, id);
    				}
    			}
    			ao2_unlock(cached);
    		}
    	}
    
    
    	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);
    }
    
    
    static int age_cmp(void *a, void *b)
    {
    	return ast_tvcmp(((struct sorcery_memory_cached_object *) b)->created,
    			((struct sorcery_memory_cached_object *) a)->created);
    }
    
    
    /*!
     * \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;
    	}
    
    
    	cache->object_heap = ast_heap_create(CACHE_HEAP_INIT_HEIGHT, age_cmp,
    		offsetof(struct sorcery_memory_cached_object, __heap_index));
    	if (!cache->object_heap) {
    		ast_log(LOG_ERROR, "Could not create heap to hold cached objects\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;
    
    	ao2_wrlock(cache->objects);
    
    	res = remove_from_cache(cache, ast_sorcery_object_get_id(object), 1);
    
    	ao2_unlock(cache->objects);
    
    	if (res) {
    		ast_log(LOG_ERROR, "Unable to delete object '%s' from sorcery cache\n", ast_sorcery_object_get_id(object));
    	}
    
    	return res;
    
    }
    
    /*!
     * \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);
    	}
    
    
    	if (cache->object_lifetime_maximum) {
    		/* If object lifetime support is enabled we need to explicitly drop all cached objects here
    		 * and stop the scheduled task. Failure to do so could potentially keep the cache around for
    		 * a prolonged period of time.
    		 */
    		ao2_wrlock(cache->objects);
    		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));
    		ao2_unlock(cache->objects);
    	}
    
    
    	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");