summaryrefslogtreecommitdiff
path: root/res/res_sorcery_memory_cache.c
diff options
context:
space:
mode:
Diffstat (limited to 'res/res_sorcery_memory_cache.c')
-rw-r--r--res/res_sorcery_memory_cache.c377
1 files changed, 375 insertions, 2 deletions
diff --git a/res/res_sorcery_memory_cache.c b/res/res_sorcery_memory_cache.c
index 2bae8886a..a37ddfd3d 100644
--- a/res/res_sorcery_memory_cache.c
+++ b/res/res_sorcery_memory_cache.c
@@ -79,6 +79,8 @@ struct sorcery_memory_cached_object {
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);
@@ -117,6 +119,50 @@ 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
@@ -493,13 +539,13 @@ static int sorcery_memory_cache_create(const struct ast_sorcery *sorcery, void *
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);
+ 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
@@ -531,6 +577,69 @@ static int sorcery_memory_cache_create(const struct ast_sorcery *sorcery, void *
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
@@ -549,11 +658,39 @@ static void *sorcery_memory_cache_retrieve_id(const struct ast_sorcery *sorcery,
struct sorcery_memory_cached_object *cached;
void *object;
+ if (is_stale_update()) {
+ return NULL;
+ }
+
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);
@@ -1462,6 +1599,240 @@ cleanup:
return res;
}
+/*!
+ * \brief Backend data that the mock sorcery wizard uses to create objects
+ */
+static struct backend_data {
+ /*! An arbitrary data field */
+ int salt;
+ /*! Another arbitrary data field */
+ int pepper;
+ /*! Indicates whether the backend has data */
+ int exists;
+} *real_backend_data;
+
+/*!
+ * \brief Sorcery object created based on backend data
+ */
+struct test_data {
+ SORCERY_OBJECT(details);
+ /*! Mirrors the backend data's salt field */
+ int salt;
+ /*! Mirrors the backend data's pepper field */
+ int pepper;
+};
+
+/*!
+ * \brief Allocation callback for test_data sorcery object
+ */
+static void *test_data_alloc(const char *id) {
+ return ast_sorcery_generic_alloc(sizeof(struct test_data), NULL);
+}
+
+/*!
+ * \brief Callback for retrieving sorcery object by ID
+ *
+ * The mock wizard uses the \ref real_backend_data in order to construct
+ * objects. If the backend data is "nonexisent" then no object is returned.
+ * Otherwise, an object is created that has the backend data's salt and
+ * pepper values copied.
+ *
+ * \param sorcery The sorcery instance
+ * \param data Unused
+ * \param type The object type. Will always be "test".
+ * \param id The object id. Will always be "test".
+ *
+ * \retval NULL Backend data does not exist
+ * \retval non-NULL An object representing the backend data
+ */
+static void *mock_retrieve_id(const struct ast_sorcery *sorcery, void *data,
+ const char *type, const char *id)
+{
+ struct test_data *b_data;
+
+ if (!real_backend_data->exists) {
+ return NULL;
+ }
+
+ b_data = ast_sorcery_alloc(sorcery, type, id);
+ if (!b_data) {
+ return NULL;
+ }
+
+ b_data->salt = real_backend_data->salt;
+ b_data->pepper = real_backend_data->pepper;
+ return b_data;
+}
+
+/*!
+ * \brief A mock sorcery wizard used for the stale test
+ */
+static struct ast_sorcery_wizard mock_wizard = {
+ .name = "mock",
+ .retrieve_id = mock_retrieve_id,
+};
+
+/*!
+ * \brief Wait for the cache to be updated after a stale object is retrieved.
+ *
+ * Since the cache does not know what type of objects it is dealing with, and
+ * since we do not have the internals of the cache, the only way to make this
+ * determination is to continuously retrieve an object from the cache until
+ * we retrieve a different object than we had previously retrieved.
+ *
+ * \param sorcery The sorcery instance
+ * \param previous_object The object we had previously retrieved from the cache
+ * \param[out] new_object The new object we retrieve from the cache
+ *
+ * \retval 0 Successfully retrieved a new object from the cache
+ * \retval non-zero Failed to retrieve a new object from the cache
+ */
+static int wait_for_cache_update(const struct ast_sorcery *sorcery,
+ void *previous_object, struct test_data **new_object)
+{
+ struct timeval start = ast_tvnow();
+
+ while (ast_remaining_ms(start, 5000) > 0) {
+ void *object;
+
+ object = ast_sorcery_retrieve_by_id(sorcery, "test", "test");
+ if (object != previous_object) {
+ *new_object = object;
+ return 0;
+ }
+ ao2_cleanup(object);
+ }
+
+ return -1;
+}
+
+AST_TEST_DEFINE(stale)
+{
+ int res = AST_TEST_FAIL;
+ struct ast_sorcery *sorcery = NULL;
+ struct test_data *backend_object;
+ struct backend_data iterations[] = {
+ { .salt = 1, .pepper = 2, .exists = 1 },
+ { .salt = 568729, .pepper = -234123, .exists = 1 },
+ { .salt = 0, .pepper = 0, .exists = 0 },
+ };
+ struct backend_data initial = {
+ .salt = 0,
+ .pepper = 0,
+ .exists = 1,
+ };
+ int i;
+
+ switch (cmd) {
+ case TEST_INIT:
+ info->name = "stale";
+ info->category = "/res/res_sorcery_memory_cache/";
+ info->summary = "Ensure that stale objects are replaced with updated objects";
+ info->description = "This test performs the following:\n"
+ "\t* Create a sorcery instance with two wizards"
+ "\t\t* The first is a memory cache that marks items stale after 3 seconds\n"
+ "\t\t* The second is a mock of a back-end\n"
+ "\t* Pre-populates the cache by retrieving some initial data from the backend.\n"
+ "\t* Performs iterations of the following:\n"
+ "\t\t* Update backend data with new values\n"
+ "\t\t* Retrieve item from the cache\n"
+ "\t\t* Ensure the retrieved item does not have the new backend values\n"
+ "\t\t* Wait for cached object to become stale\n"
+ "\t\t* Retrieve the stale cached object\n"
+ "\t\t* Ensure that the stale object retrieved is the same as the fresh one from earlier\n"
+ "\t\t* Wait for the cache to update with new data\n"
+ "\t\t* Ensure that new data in the cache matches backend data\n";
+ return AST_TEST_NOT_RUN;
+ case TEST_EXECUTE:
+ break;
+ }
+
+ ast_sorcery_wizard_register(&mock_wizard);
+
+ sorcery = ast_sorcery_open();
+ if (!sorcery) {
+ ast_test_status_update(test, "Failed to create sorcery instance\n");
+ goto cleanup;
+ }
+
+ ast_sorcery_apply_wizard_mapping(sorcery, "test", "memory_cache",
+ "object_lifetime_stale=3", 1);
+ ast_sorcery_apply_wizard_mapping(sorcery, "test", "mock", NULL, 0);
+ ast_sorcery_internal_object_register(sorcery, "test", test_data_alloc, NULL, NULL);
+
+ /* Prepopulate the cache */
+ real_backend_data = &initial;
+
+ backend_object = ast_sorcery_retrieve_by_id(sorcery, "test", "test");
+ if (!backend_object) {
+ ast_test_status_update(test, "Unable to retrieve backend data and populate the cache\n");
+ goto cleanup;
+ }
+ ao2_ref(backend_object, -1);
+
+ for (i = 0; i < ARRAY_LEN(iterations); ++i) {
+ RAII_VAR(struct test_data *, cache_fresh, NULL, ao2_cleanup);
+ RAII_VAR(struct test_data *, cache_stale, NULL, ao2_cleanup);
+ RAII_VAR(struct test_data *, cache_new, NULL, ao2_cleanup);
+
+ real_backend_data = &iterations[i];
+
+ ast_test_status_update(test, "Begininning iteration %d\n", i);
+
+ cache_fresh = ast_sorcery_retrieve_by_id(sorcery, "test", "test");
+ if (!cache_fresh) {
+ ast_test_status_update(test, "Unable to retrieve fresh cached object\n");
+ goto cleanup;
+ }
+
+ if (cache_fresh->salt == iterations[i].salt || cache_fresh->pepper == iterations[i].pepper) {
+ ast_test_status_update(test, "Fresh cached object has unexpected values. Did we hit the backend?\n");
+ goto cleanup;
+ }
+
+ sleep(5);
+
+ cache_stale = ast_sorcery_retrieve_by_id(sorcery, "test", "test");
+ if (!cache_stale) {
+ ast_test_status_update(test, "Unable to retrieve stale cached object\n");
+ goto cleanup;
+ }
+
+ if (cache_stale != cache_fresh) {
+ ast_test_status_update(test, "Stale cache hit retrieved different object than fresh cache hit\n");
+ goto cleanup;
+ }
+
+ if (wait_for_cache_update(sorcery, cache_stale, &cache_new)) {
+ ast_test_status_update(test, "Cache was not updated\n");
+ goto cleanup;
+ }
+
+ if (iterations[i].exists) {
+ if (!cache_new) {
+ ast_test_status_update(test, "Failed to retrieve item from cache when there should be one present\n");
+ goto cleanup;
+ } else if (cache_new->salt != iterations[i].salt ||
+ cache_new->pepper != iterations[i].pepper) {
+ ast_test_status_update(test, "New cached item has unexpected values\n");
+ goto cleanup;
+ }
+ } else if (cache_new) {
+ ast_test_status_update(test, "Retrieved a cached item when there should not have been one present\n");
+ goto cleanup;
+ }
+ }
+
+ res = AST_TEST_PASS;
+
+cleanup:
+ if (sorcery) {
+ ast_sorcery_unref(sorcery);
+ }
+ ast_sorcery_wizard_unregister(&mock_wizard);
+ return res;
+}
+
#endif
static int unload_module(void)
@@ -1482,6 +1853,7 @@ static int unload_module(void)
AST_TEST_UNREGISTER(delete);
AST_TEST_UNREGISTER(maximum_objects);
AST_TEST_UNREGISTER(expiration);
+ AST_TEST_UNREGISTER(stale);
return 0;
}
@@ -1521,6 +1893,7 @@ static int load_module(void)
AST_TEST_REGISTER(delete);
AST_TEST_REGISTER(maximum_objects);
AST_TEST_REGISTER(expiration);
+ AST_TEST_REGISTER(stale);
return AST_MODULE_LOAD_SUCCESS;
}