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