diff --git a/res/res_sorcery_memory_cache.c b/res/res_sorcery_memory_cache.c index 2bae8886a2..a37ddfd3dd 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; }