res_sorcery_memory_cache: Add support for refreshing stale objects.
[asterisk/asterisk.git] / res / res_sorcery_memory_cache.c
index 2bae888..a37ddfd 100644 (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;
 }