Merge "res_sorcery_memory_cache: Add support for refreshing stale objects."

This commit is contained in:
Joshua Colp 2015-06-01 13:01:17 -05:00 committed by Gerrit Code Review
commit 34bb5ca97c
1 changed files with 375 additions and 2 deletions

View File

@ -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;
}