|
|
|
@ -57,6 +57,18 @@ struct sorcery_memory_cache {
|
|
|
|
|
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 */
|
|
|
|
@ -265,6 +277,8 @@ static void sorcery_memory_cached_object_destructor(void *obj)
|
|
|
|
|
ao2_cleanup(cached->object);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int schedule_cache_expiration(struct sorcery_memory_cache *cache);
|
|
|
|
|
|
|
|
|
|
/*!
|
|
|
|
|
* \internal
|
|
|
|
|
* \brief Remove an object from the cache.
|
|
|
|
@ -275,13 +289,15 @@ static void sorcery_memory_cached_object_destructor(void *obj)
|
|
|
|
|
*
|
|
|
|
|
* \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)
|
|
|
|
|
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,
|
|
|
|
@ -289,11 +305,111 @@ static int remove_from_cache(struct sorcery_memory_cache *cache, const char *id)
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -323,6 +439,9 @@ static int remove_oldest_from_cache(struct sorcery_memory_cache *cache)
|
|
|
|
|
ast_assert(heap_old_object == hash_old_object);
|
|
|
|
|
|
|
|
|
|
ao2_ref(hash_old_object, -1);
|
|
|
|
|
|
|
|
|
|
schedule_cache_expiration(cache);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -344,12 +463,17 @@ static int add_to_cache(struct sorcery_memory_cache *cache,
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -384,7 +508,7 @@ static int sorcery_memory_cache_create(const struct ast_sorcery *sorcery, void *
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
ao2_wrlock(cache->objects);
|
|
|
|
|
remove_from_cache(cache, ast_sorcery_object_get_id(object));
|
|
|
|
|
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",
|
|
|
|
@ -514,6 +638,8 @@ static void *sorcery_memory_cache_open(const char *data)
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cache->expire_id = -1;
|
|
|
|
|
|
|
|
|
|
/* If no configuration options have been provided this memory cache will operate in a default
|
|
|
|
|
* configuration.
|
|
|
|
|
*/
|
|
|
|
@ -597,7 +723,7 @@ static int sorcery_memory_cache_delete(const struct ast_sorcery *sorcery, void *
|
|
|
|
|
int res;
|
|
|
|
|
|
|
|
|
|
ao2_wrlock(cache->objects);
|
|
|
|
|
res = remove_from_cache(cache, ast_sorcery_object_get_id(object));
|
|
|
|
|
res = remove_from_cache(cache, ast_sorcery_object_get_id(object), 1);
|
|
|
|
|
ao2_unlock(cache->objects);
|
|
|
|
|
|
|
|
|
|
if (res) {
|
|
|
|
@ -622,6 +748,18 @@ static void sorcery_memory_cache_close(void *data)
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1235,6 +1373,95 @@ cleanup:
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AST_TEST_DEFINE(expiration)
|
|
|
|
|
{
|
|
|
|
|
int res = AST_TEST_FAIL;
|
|
|
|
|
struct ast_sorcery *sorcery = NULL;
|
|
|
|
|
struct sorcery_memory_cache *cache = NULL;
|
|
|
|
|
int i;
|
|
|
|
|
|
|
|
|
|
switch (cmd) {
|
|
|
|
|
case TEST_INIT:
|
|
|
|
|
info->name = "expiration";
|
|
|
|
|
info->category = "/res/res_sorcery_memory_cache/";
|
|
|
|
|
info->summary = "Add objects to a cache configured with maximum lifetime, confirm they are removed";
|
|
|
|
|
info->description = "This test performs the following:\n"
|
|
|
|
|
"\t* Creates a memory cache with a maximum object lifetime of 5 seconds\n"
|
|
|
|
|
"\t* Pushes 10 objects into the memory cache\n"
|
|
|
|
|
"\t* Waits (up to) 10 seconds for expiration to occur\n"
|
|
|
|
|
"\t* Confirms that the objects have been removed from the cache\n";
|
|
|
|
|
return AST_TEST_NOT_RUN;
|
|
|
|
|
case TEST_EXECUTE:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cache = sorcery_memory_cache_open("object_lifetime_maximum=5");
|
|
|
|
|
if (!cache) {
|
|
|
|
|
ast_test_status_update(test, "Failed to create a sorcery memory cache using default options\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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cache->cache_notify = 1;
|
|
|
|
|
ast_mutex_init(&cache->lock);
|
|
|
|
|
ast_cond_init(&cache->cond, NULL);
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < 5; ++i) {
|
|
|
|
|
char uuid[AST_UUID_STR_LEN];
|
|
|
|
|
void *object;
|
|
|
|
|
|
|
|
|
|
object = ast_sorcery_alloc(sorcery, "test", ast_uuid_generate_str(uuid, sizeof(uuid)));
|
|
|
|
|
if (!object) {
|
|
|
|
|
ast_test_status_update(test, "Failed to allocate test object for expiration\n");
|
|
|
|
|
goto cleanup;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sorcery_memory_cache_create(sorcery, cache, object);
|
|
|
|
|
|
|
|
|
|
ao2_ref(object, -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ast_mutex_lock(&cache->lock);
|
|
|
|
|
while (!cache->cache_completed) {
|
|
|
|
|
struct timeval start = ast_tvnow();
|
|
|
|
|
struct timespec end = {
|
|
|
|
|
.tv_sec = start.tv_sec + 10,
|
|
|
|
|
.tv_nsec = start.tv_usec * 1000,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (ast_cond_timedwait(&cache->cond, &cache->lock, &end) == ETIMEDOUT) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ast_mutex_unlock(&cache->lock);
|
|
|
|
|
|
|
|
|
|
if (ao2_container_count(cache->objects)) {
|
|
|
|
|
ast_test_status_update(test, "Objects placed into the memory cache did not expire and get removed\n");
|
|
|
|
|
goto cleanup;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res = AST_TEST_PASS;
|
|
|
|
|
|
|
|
|
|
cleanup:
|
|
|
|
|
if (cache) {
|
|
|
|
|
if (cache->cache_notify) {
|
|
|
|
|
ast_cond_destroy(&cache->cond);
|
|
|
|
|
ast_mutex_destroy(&cache->lock);
|
|
|
|
|
}
|
|
|
|
|
sorcery_memory_cache_close(cache);
|
|
|
|
|
}
|
|
|
|
|
if (sorcery) {
|
|
|
|
|
ast_sorcery_unref(sorcery);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static int unload_module(void)
|
|
|
|
@ -1254,6 +1481,7 @@ static int unload_module(void)
|
|
|
|
|
AST_TEST_UNREGISTER(update);
|
|
|
|
|
AST_TEST_UNREGISTER(delete);
|
|
|
|
|
AST_TEST_UNREGISTER(maximum_objects);
|
|
|
|
|
AST_TEST_UNREGISTER(expiration);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
@ -1292,6 +1520,7 @@ static int load_module(void)
|
|
|
|
|
AST_TEST_REGISTER(update);
|
|
|
|
|
AST_TEST_REGISTER(delete);
|
|
|
|
|
AST_TEST_REGISTER(maximum_objects);
|
|
|
|
|
AST_TEST_REGISTER(expiration);
|
|
|
|
|
|
|
|
|
|
return AST_MODULE_LOAD_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|