blob: aee0914d604a5f452627eeb57d3ba1952e3a9486 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright 2023 Red Hat
*/
#include "index-session.h"
#include <linux/atomic.h>
#include "logger.h"
#include "memory-alloc.h"
#include "time-utils.h"
#include "funnel-requestqueue.h"
#include "index.h"
#include "index-layout.h"
/*
* The index session contains a lock (the request_mutex) which ensures that only one thread can
* change the state of its index at a time. The state field indicates the current state of the
* index through a set of descriptive flags. The request_mutex must be notified whenever a
* non-transient state flag is cleared. The request_mutex is also used to count the number of
* requests currently in progress so that they can be drained when suspending or closing the index.
*
* If the index session is suspended shortly after opening an index, it may have to suspend during
* a rebuild. Depending on the size of the index, a rebuild may take a significant amount of time,
* so UDS allows the rebuild to be paused in order to suspend the session in a timely manner. When
* the index session is resumed, the rebuild can continue from where it left off. If the index
* session is shut down with a suspended rebuild, the rebuild progress is abandoned and the rebuild
* will start from the beginning the next time the index is loaded. The mutex and status fields in
* the index_load_context are used to record the state of any interrupted rebuild.
*/
enum index_session_flag_bit {
IS_FLAG_BIT_START = 8,
/* The session has started loading an index but not completed it. */
IS_FLAG_BIT_LOADING = IS_FLAG_BIT_START,
/* The session has loaded an index, which can handle requests. */
IS_FLAG_BIT_LOADED,
/* The session's index has been permanently disabled. */
IS_FLAG_BIT_DISABLED,
/* The session's index is suspended. */
IS_FLAG_BIT_SUSPENDED,
/* The session is handling some index state change. */
IS_FLAG_BIT_WAITING,
/* The session's index is closing and draining requests. */
IS_FLAG_BIT_CLOSING,
/* The session is being destroyed and is draining requests. */
IS_FLAG_BIT_DESTROYING,
};
enum index_session_flag {
IS_FLAG_LOADED = (1 << IS_FLAG_BIT_LOADED),
IS_FLAG_LOADING = (1 << IS_FLAG_BIT_LOADING),
IS_FLAG_DISABLED = (1 << IS_FLAG_BIT_DISABLED),
IS_FLAG_SUSPENDED = (1 << IS_FLAG_BIT_SUSPENDED),
IS_FLAG_WAITING = (1 << IS_FLAG_BIT_WAITING),
IS_FLAG_CLOSING = (1 << IS_FLAG_BIT_CLOSING),
IS_FLAG_DESTROYING = (1 << IS_FLAG_BIT_DESTROYING),
};
/* Release a reference to an index session. */
static void release_index_session(struct uds_index_session *index_session)
{
mutex_lock(&index_session->request_mutex);
if (--index_session->request_count == 0)
uds_broadcast_cond(&index_session->request_cond);
mutex_unlock(&index_session->request_mutex);
}
/*
* Acquire a reference to the index session for an asynchronous index request. The reference must
* eventually be released with a corresponding call to release_index_session().
*/
static int get_index_session(struct uds_index_session *index_session)
{
unsigned int state;
int result = UDS_SUCCESS;
mutex_lock(&index_session->request_mutex);
index_session->request_count++;
state = index_session->state;
mutex_unlock(&index_session->request_mutex);
if (state == IS_FLAG_LOADED) {
return UDS_SUCCESS;
} else if (state & IS_FLAG_DISABLED) {
result = UDS_DISABLED;
} else if ((state & IS_FLAG_LOADING) ||
(state & IS_FLAG_SUSPENDED) ||
(state & IS_FLAG_WAITING)) {
result = -EBUSY;
} else {
result = UDS_NO_INDEX;
}
release_index_session(index_session);
return result;
}
int uds_launch_request(struct uds_request *request)
{
size_t internal_size;
int result;
if (request->callback == NULL) {
vdo_log_error("missing required callback");
return -EINVAL;
}
switch (request->type) {
case UDS_DELETE:
case UDS_POST:
case UDS_QUERY:
case UDS_QUERY_NO_UPDATE:
case UDS_UPDATE:
break;
default:
vdo_log_error("received invalid callback type");
return -EINVAL;
}
/* Reset all internal fields before processing. */
internal_size =
sizeof(struct uds_request) - offsetof(struct uds_request, zone_number);
// FIXME should be using struct_group for this instead
memset((char *) request + sizeof(*request) - internal_size, 0, internal_size);
result = get_index_session(request->session);
if (result != UDS_SUCCESS)
return result;
request->found = false;
request->unbatched = false;
request->index = request->session->index;
uds_enqueue_request(request, STAGE_TRIAGE);
return UDS_SUCCESS;
}
static void enter_callback_stage(struct uds_request *request)
{
if (request->status != UDS_SUCCESS) {
/* All request errors are considered unrecoverable */
mutex_lock(&request->session->request_mutex);
request->session->state |= IS_FLAG_DISABLED;
mutex_unlock(&request->session->request_mutex);
}
uds_request_queue_enqueue(request->session->callback_queue, request);
}
static inline void count_once(u64 *count_ptr)
{
WRITE_ONCE(*count_ptr, READ_ONCE(*count_ptr) + 1);
}
static void update_session_stats(struct uds_request *request)
{
struct session_stats *session_stats = &request->session->stats;
count_once(&session_stats->requests);
switch (request->type) {
case UDS_POST:
if (request->found)
count_once(&session_stats->posts_found);
else
count_once(&session_stats->posts_not_found);
if (request->location == UDS_LOCATION_IN_OPEN_CHAPTER)
count_once(&session_stats->posts_found_open_chapter);
else if (request->location == UDS_LOCATION_IN_DENSE)
count_once(&session_stats->posts_found_dense);
else if (request->location == UDS_LOCATION_IN_SPARSE)
count_once(&session_stats->posts_found_sparse);
break;
case UDS_UPDATE:
if (request->found)
count_once(&session_stats->updates_found);
else
count_once(&session_stats->updates_not_found);
break;
case UDS_DELETE:
if (request->found)
count_once(&session_stats->deletions_found);
else
count_once(&session_stats->deletions_not_found);
break;
case UDS_QUERY:
case UDS_QUERY_NO_UPDATE:
if (request->found)
count_once(&session_stats->queries_found);
else
count_once(&session_stats->queries_not_found);
break;
default:
request->status = VDO_ASSERT(false, "unknown request type: %d",
request->type);
}
}
static void handle_callbacks(struct uds_request *request)
{
struct uds_index_session *index_session = request->session;
if (request->status == UDS_SUCCESS)
update_session_stats(request);
request->status = uds_status_to_errno(request->status);
request->callback(request);
release_index_session(index_session);
}
static int __must_check make_empty_index_session(struct uds_index_session **index_session_ptr)
{
int result;
struct uds_index_session *session;
result = vdo_allocate(1, struct uds_index_session, __func__, &session);
if (result != VDO_SUCCESS)
return result;
mutex_init(&session->request_mutex);
uds_init_cond(&session->request_cond);
mutex_init(&session->load_context.mutex);
uds_init_cond(&session->load_context.cond);
result = uds_make_request_queue("callbackW", &handle_callbacks,
&session->callback_queue);
if (result != UDS_SUCCESS) {
vdo_free(session);
return result;
}
*index_session_ptr = session;
return UDS_SUCCESS;
}
int uds_create_index_session(struct uds_index_session **session)
{
if (session == NULL) {
vdo_log_error("missing session pointer");
return -EINVAL;
}
return uds_status_to_errno(make_empty_index_session(session));
}
static int __must_check start_loading_index_session(struct uds_index_session *index_session)
{
int result;
mutex_lock(&index_session->request_mutex);
if (index_session->state & IS_FLAG_SUSPENDED) {
vdo_log_info("Index session is suspended");
result = -EBUSY;
} else if (index_session->state != 0) {
vdo_log_info("Index is already loaded");
result = -EBUSY;
} else {
index_session->state |= IS_FLAG_LOADING;
result = UDS_SUCCESS;
}
mutex_unlock(&index_session->request_mutex);
return result;
}
static void finish_loading_index_session(struct uds_index_session *index_session,
int result)
{
mutex_lock(&index_session->request_mutex);
index_session->state &= ~IS_FLAG_LOADING;
if (result == UDS_SUCCESS)
index_session->state |= IS_FLAG_LOADED;
uds_broadcast_cond(&index_session->request_cond);
mutex_unlock(&index_session->request_mutex);
}
static int initialize_index_session(struct uds_index_session *index_session,
enum uds_open_index_type open_type)
{
int result;
struct uds_configuration *config;
result = uds_make_configuration(&index_session->parameters, &config);
if (result != UDS_SUCCESS) {
vdo_log_error_strerror(result, "Failed to allocate config");
return result;
}
memset(&index_session->stats, 0, sizeof(index_session->stats));
result = uds_make_index(config, open_type, &index_session->load_context,
enter_callback_stage, &index_session->index);
if (result != UDS_SUCCESS)
vdo_log_error_strerror(result, "Failed to make index");
else
uds_log_configuration(config);
uds_free_configuration(config);
return result;
}
static const char *get_open_type_string(enum uds_open_index_type open_type)
{
switch (open_type) {
case UDS_CREATE:
return "creating index";
case UDS_LOAD:
return "loading or rebuilding index";
case UDS_NO_REBUILD:
return "loading index";
default:
return "unknown open method";
}
}
/*
* Open an index under the given session. This operation will fail if the
* index session is suspended, or if there is already an open index.
*/
int uds_open_index(enum uds_open_index_type open_type,
const struct uds_parameters *parameters,
struct uds_index_session *session)
{
int result;
char name[BDEVNAME_SIZE];
if (parameters == NULL) {
vdo_log_error("missing required parameters");
return -EINVAL;
}
if (parameters->bdev == NULL) {
vdo_log_error("missing required block device");
return -EINVAL;
}
if (session == NULL) {
vdo_log_error("missing required session pointer");
return -EINVAL;
}
result = start_loading_index_session(session);
if (result != UDS_SUCCESS)
return uds_status_to_errno(result);
session->parameters = *parameters;
format_dev_t(name, parameters->bdev->bd_dev);
vdo_log_info("%s: %s", get_open_type_string(open_type), name);
result = initialize_index_session(session, open_type);
if (result != UDS_SUCCESS)
vdo_log_error_strerror(result, "Failed %s",
get_open_type_string(open_type));
finish_loading_index_session(session, result);
return uds_status_to_errno(result);
}
static void wait_for_no_requests_in_progress(struct uds_index_session *index_session)
{
mutex_lock(&index_session->request_mutex);
while (index_session->request_count > 0) {
uds_wait_cond(&index_session->request_cond,
&index_session->request_mutex);
}
mutex_unlock(&index_session->request_mutex);
}
static int __must_check save_index(struct uds_index_session *index_session)
{
wait_for_no_requests_in_progress(index_session);
return uds_save_index(index_session->index);
}
static void suspend_rebuild(struct uds_index_session *session)
{
mutex_lock(&session->load_context.mutex);
switch (session->load_context.status) {
case INDEX_OPENING:
session->load_context.status = INDEX_SUSPENDING;
/* Wait until the index indicates that it is not replaying. */
while ((session->load_context.status != INDEX_SUSPENDED) &&
(session->load_context.status != INDEX_READY)) {
uds_wait_cond(&session->load_context.cond,
&session->load_context.mutex);
}
break;
case INDEX_READY:
/* Index load does not need to be suspended. */
break;
case INDEX_SUSPENDED:
case INDEX_SUSPENDING:
case INDEX_FREEING:
default:
/* These cases should not happen. */
VDO_ASSERT_LOG_ONLY(false, "Bad load context state %u",
session->load_context.status);
break;
}
mutex_unlock(&session->load_context.mutex);
}
/*
* Suspend index operation, draining all current index requests and preventing new index requests
* from starting. Optionally saves all index data before returning.
*/
int uds_suspend_index_session(struct uds_index_session *session, bool save)
{
int result = UDS_SUCCESS;
bool no_work = false;
bool rebuilding = false;
/* Wait for any current index state change to complete. */
mutex_lock(&session->request_mutex);
while (session->state & IS_FLAG_CLOSING)
uds_wait_cond(&session->request_cond, &session->request_mutex);
if ((session->state & IS_FLAG_WAITING) || (session->state & IS_FLAG_DESTROYING)) {
no_work = true;
vdo_log_info("Index session is already changing state");
result = -EBUSY;
} else if (session->state & IS_FLAG_SUSPENDED) {
no_work = true;
} else if (session->state & IS_FLAG_LOADING) {
session->state |= IS_FLAG_WAITING;
rebuilding = true;
} else if (session->state & IS_FLAG_LOADED) {
session->state |= IS_FLAG_WAITING;
} else {
no_work = true;
session->state |= IS_FLAG_SUSPENDED;
uds_broadcast_cond(&session->request_cond);
}
mutex_unlock(&session->request_mutex);
if (no_work)
return uds_status_to_errno(result);
if (rebuilding)
suspend_rebuild(session);
else if (save)
result = save_index(session);
else
result = uds_flush_index_session(session);
mutex_lock(&session->request_mutex);
session->state &= ~IS_FLAG_WAITING;
session->state |= IS_FLAG_SUSPENDED;
uds_broadcast_cond(&session->request_cond);
mutex_unlock(&session->request_mutex);
return uds_status_to_errno(result);
}
static int replace_device(struct uds_index_session *session, struct block_device *bdev)
{
int result;
result = uds_replace_index_storage(session->index, bdev);
if (result != UDS_SUCCESS)
return result;
session->parameters.bdev = bdev;
return UDS_SUCCESS;
}
/*
* Resume index operation after being suspended. If the index is suspended and the supplied block
* device differs from the current backing store, the index will start using the new backing store.
*/
int uds_resume_index_session(struct uds_index_session *session,
struct block_device *bdev)
{
int result = UDS_SUCCESS;
bool no_work = false;
bool resume_replay = false;
mutex_lock(&session->request_mutex);
if (session->state & IS_FLAG_WAITING) {
vdo_log_info("Index session is already changing state");
no_work = true;
result = -EBUSY;
} else if (!(session->state & IS_FLAG_SUSPENDED)) {
/* If not suspended, just succeed. */
no_work = true;
result = UDS_SUCCESS;
} else {
session->state |= IS_FLAG_WAITING;
if (session->state & IS_FLAG_LOADING)
resume_replay = true;
}
mutex_unlock(&session->request_mutex);
if (no_work)
return result;
if ((session->index != NULL) && (bdev != session->parameters.bdev)) {
result = replace_device(session, bdev);
if (result != UDS_SUCCESS) {
mutex_lock(&session->request_mutex);
session->state &= ~IS_FLAG_WAITING;
uds_broadcast_cond(&session->request_cond);
mutex_unlock(&session->request_mutex);
return uds_status_to_errno(result);
}
}
if (resume_replay) {
mutex_lock(&session->load_context.mutex);
switch (session->load_context.status) {
case INDEX_SUSPENDED:
session->load_context.status = INDEX_OPENING;
/* Notify the index to start replaying again. */
uds_broadcast_cond(&session->load_context.cond);
break;
case INDEX_READY:
/* There is no index rebuild to resume. */
break;
case INDEX_OPENING:
case INDEX_SUSPENDING:
case INDEX_FREEING:
default:
/* These cases should not happen; do nothing. */
VDO_ASSERT_LOG_ONLY(false, "Bad load context state %u",
session->load_context.status);
break;
}
mutex_unlock(&session->load_context.mutex);
}
mutex_lock(&session->request_mutex);
session->state &= ~IS_FLAG_WAITING;
session->state &= ~IS_FLAG_SUSPENDED;
uds_broadcast_cond(&session->request_cond);
mutex_unlock(&session->request_mutex);
return UDS_SUCCESS;
}
static int save_and_free_index(struct uds_index_session *index_session)
{
int result = UDS_SUCCESS;
bool suspended;
struct uds_index *index = index_session->index;
if (index == NULL)
return UDS_SUCCESS;
mutex_lock(&index_session->request_mutex);
suspended = (index_session->state & IS_FLAG_SUSPENDED);
mutex_unlock(&index_session->request_mutex);
if (!suspended) {
result = uds_save_index(index);
if (result != UDS_SUCCESS)
vdo_log_warning_strerror(result,
"ignoring error from save_index");
}
uds_free_index(index);
index_session->index = NULL;
/*
* Reset all index state that happens to be in the index
* session, so it doesn't affect any future index.
*/
mutex_lock(&index_session->load_context.mutex);
index_session->load_context.status = INDEX_OPENING;
mutex_unlock(&index_session->load_context.mutex);
mutex_lock(&index_session->request_mutex);
/* Only the suspend bit will remain relevant. */
index_session->state &= IS_FLAG_SUSPENDED;
mutex_unlock(&index_session->request_mutex);
return result;
}
/* Save and close the current index. */
int uds_close_index(struct uds_index_session *index_session)
{
int result = UDS_SUCCESS;
/* Wait for any current index state change to complete. */
mutex_lock(&index_session->request_mutex);
while ((index_session->state & IS_FLAG_WAITING) ||
(index_session->state & IS_FLAG_CLOSING)) {
uds_wait_cond(&index_session->request_cond,
&index_session->request_mutex);
}
if (index_session->state & IS_FLAG_SUSPENDED) {
vdo_log_info("Index session is suspended");
result = -EBUSY;
} else if ((index_session->state & IS_FLAG_DESTROYING) ||
!(index_session->state & IS_FLAG_LOADED)) {
/* The index doesn't exist, hasn't finished loading, or is being destroyed. */
result = UDS_NO_INDEX;
} else {
index_session->state |= IS_FLAG_CLOSING;
}
mutex_unlock(&index_session->request_mutex);
if (result != UDS_SUCCESS)
return uds_status_to_errno(result);
vdo_log_debug("Closing index");
wait_for_no_requests_in_progress(index_session);
result = save_and_free_index(index_session);
vdo_log_debug("Closed index");
mutex_lock(&index_session->request_mutex);
index_session->state &= ~IS_FLAG_CLOSING;
uds_broadcast_cond(&index_session->request_cond);
mutex_unlock(&index_session->request_mutex);
return uds_status_to_errno(result);
}
/* This will save and close an open index before destroying the session. */
int uds_destroy_index_session(struct uds_index_session *index_session)
{
int result;
bool load_pending = false;
vdo_log_debug("Destroying index session");
/* Wait for any current index state change to complete. */
mutex_lock(&index_session->request_mutex);
while ((index_session->state & IS_FLAG_WAITING) ||
(index_session->state & IS_FLAG_CLOSING)) {
uds_wait_cond(&index_session->request_cond,
&index_session->request_mutex);
}
if (index_session->state & IS_FLAG_DESTROYING) {
mutex_unlock(&index_session->request_mutex);
vdo_log_info("Index session is already closing");
return -EBUSY;
}
index_session->state |= IS_FLAG_DESTROYING;
load_pending = ((index_session->state & IS_FLAG_LOADING) &&
(index_session->state & IS_FLAG_SUSPENDED));
mutex_unlock(&index_session->request_mutex);
if (load_pending) {
/* Tell the index to terminate the rebuild. */
mutex_lock(&index_session->load_context.mutex);
if (index_session->load_context.status == INDEX_SUSPENDED) {
index_session->load_context.status = INDEX_FREEING;
uds_broadcast_cond(&index_session->load_context.cond);
}
mutex_unlock(&index_session->load_context.mutex);
/* Wait until the load exits before proceeding. */
mutex_lock(&index_session->request_mutex);
while (index_session->state & IS_FLAG_LOADING) {
uds_wait_cond(&index_session->request_cond,
&index_session->request_mutex);
}
mutex_unlock(&index_session->request_mutex);
}
wait_for_no_requests_in_progress(index_session);
result = save_and_free_index(index_session);
uds_request_queue_finish(index_session->callback_queue);
index_session->callback_queue = NULL;
vdo_log_debug("Destroyed index session");
vdo_free(index_session);
return uds_status_to_errno(result);
}
/* Wait until all callbacks for index operations are complete. */
int uds_flush_index_session(struct uds_index_session *index_session)
{
wait_for_no_requests_in_progress(index_session);
uds_wait_for_idle_index(index_session->index);
return UDS_SUCCESS;
}
/* Statistics collection is intended to be thread-safe. */
static void collect_stats(const struct uds_index_session *index_session,
struct uds_index_stats *stats)
{
const struct session_stats *session_stats = &index_session->stats;
stats->current_time = ktime_to_seconds(current_time_ns(CLOCK_REALTIME));
stats->posts_found = READ_ONCE(session_stats->posts_found);
stats->in_memory_posts_found = READ_ONCE(session_stats->posts_found_open_chapter);
stats->dense_posts_found = READ_ONCE(session_stats->posts_found_dense);
stats->sparse_posts_found = READ_ONCE(session_stats->posts_found_sparse);
stats->posts_not_found = READ_ONCE(session_stats->posts_not_found);
stats->updates_found = READ_ONCE(session_stats->updates_found);
stats->updates_not_found = READ_ONCE(session_stats->updates_not_found);
stats->deletions_found = READ_ONCE(session_stats->deletions_found);
stats->deletions_not_found = READ_ONCE(session_stats->deletions_not_found);
stats->queries_found = READ_ONCE(session_stats->queries_found);
stats->queries_not_found = READ_ONCE(session_stats->queries_not_found);
stats->requests = READ_ONCE(session_stats->requests);
}
int uds_get_index_session_stats(struct uds_index_session *index_session,
struct uds_index_stats *stats)
{
if (stats == NULL) {
vdo_log_error("received a NULL index stats pointer");
return -EINVAL;
}
collect_stats(index_session, stats);
if (index_session->index != NULL) {
uds_get_index_stats(index_session->index, stats);
} else {
stats->entries_indexed = 0;
stats->memory_used = 0;
stats->collisions = 0;
stats->entries_discarded = 0;
}
return UDS_SUCCESS;
}
void uds_wait_cond(struct cond_var *cv, struct mutex *mutex)
{
DEFINE_WAIT(__wait);
prepare_to_wait(&cv->wait_queue, &__wait, TASK_IDLE);
mutex_unlock(mutex);
schedule();
finish_wait(&cv->wait_queue, &__wait);
mutex_lock(mutex);
}