[asterisk-commits] res sorcery memory cache: Add support for object lifetime ma... (asterisk[master])

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Fri May 22 11:55:24 CDT 2015


Mark Michelson has submitted this change and it was merged.

Change subject: res_sorcery_memory_cache: Add support for object_lifetime_maximum.
......................................................................


res_sorcery_memory_cache: Add support for object_lifetime_maximum.

This makes the "object_lifetime_maximum" option operational.

On the addition of an object to an empty memory cache a scheduled
task is created which, when invoked, expires objects from the cache
which have exceeded their lifetime. If more objects have been added
the remaining life of the oldest object is used to schedule the
next invocation of the scheduled task.

If the oldest object is removed from the cache before it can be
expired automatically the scheduled task is cancelled, if possible,
and the lifetime of the next oldest is used to schedule the task.

If during these two operations no additional objects exist in the
cache then no task is scheduled.

An additional unit test has been added which verifies this
functionality.

ASTERISK-25067
Reported by: Matt Jordan

Change-Id: I87409674674a508e7717ee20739ca15cec6ba7b6
---
M res/res_sorcery_memory_cache.c
1 file changed, 232 insertions(+), 3 deletions(-)

Approvals:
  Mark Michelson: Looks good to me, approved; Verified
  Matt Jordan: Looks good to me, but someone else must approve



diff --git a/res/res_sorcery_memory_cache.c b/res/res_sorcery_memory_cache.c
index c825d2c..2bae888 100644
--- a/res/res_sorcery_memory_cache.c
+++ b/res/res_sorcery_memory_cache.c
@@ -57,6 +57,18 @@
 	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 @@
 	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 @@
  *
  * \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 @@
 	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 @@
 	ast_assert(heap_old_object == hash_old_object);
 
 	ao2_ref(hash_old_object, -1);
+
+	schedule_cache_expiration(cache);
+
 	return 0;
 }
 
@@ -344,10 +463,15 @@
 	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 @@
 	 */
 
 	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 @@
 		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 @@
 	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) {
@@ -620,6 +746,18 @@
 	/* 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);
@@ -1235,6 +1373,95 @@
 	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 @@
 	AST_TEST_UNREGISTER(update);
 	AST_TEST_UNREGISTER(delete);
 	AST_TEST_UNREGISTER(maximum_objects);
+	AST_TEST_UNREGISTER(expiration);
 
 	return 0;
 }
@@ -1292,6 +1520,7 @@
 	AST_TEST_REGISTER(update);
 	AST_TEST_REGISTER(delete);
 	AST_TEST_REGISTER(maximum_objects);
+	AST_TEST_REGISTER(expiration);
 
 	return AST_MODULE_LOAD_SUCCESS;
 }

-- 
To view, visit https://gerrit.asterisk.org/499
To unsubscribe, visit https://gerrit.asterisk.org/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I87409674674a508e7717ee20739ca15cec6ba7b6
Gerrit-PatchSet: 4
Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Owner: Joshua Colp <jcolp at digium.com>
Gerrit-Reviewer: Mark Michelson <mmichelson at digium.com>
Gerrit-Reviewer: Matt Jordan <mjordan at digium.com>



More information about the asterisk-commits mailing list