diff options
Diffstat (limited to 'src/plugins/old-stats/stats-plugin.c')
-rw-r--r-- | src/plugins/old-stats/stats-plugin.c | 481 |
1 files changed, 481 insertions, 0 deletions
diff --git a/src/plugins/old-stats/stats-plugin.c b/src/plugins/old-stats/stats-plugin.c new file mode 100644 index 0000000..9d33016 --- /dev/null +++ b/src/plugins/old-stats/stats-plugin.c @@ -0,0 +1,481 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "llist.h" +#include "str.h" +#include "time-util.h" +#include "settings-parser.h" +#include "mail-stats.h" +#include "stats.h" +#include "mail-stats-connection.h" +#include "stats-plugin.h" + +#define STATS_CONTEXT(obj) \ + MODULE_CONTEXT(obj, stats_storage_module) +#define STATS_CONTEXT_REQUIRE(obj) \ + MODULE_CONTEXT_REQUIRE(obj, stats_storage_module) + +/* If session isn't refreshed every 15 minutes, it's dropped. + Must be smaller than MAIL_SESSION_IDLE_TIMEOUT_MSECS in stats server */ +#define SESSION_STATS_FORCE_REFRESH_SECS (5*60) +#define REFRESH_CHECK_INTERVAL 100 +#define MAIL_STATS_FIFO_NAME "old-stats-mail" + +struct stats_storage { + union mail_storage_module_context module_ctx; + + struct mail_storage_callbacks old_callbacks; + void *old_context; +}; + +struct stats_mailbox { + union mailbox_module_context module_ctx; +}; + +const char *stats_plugin_version = DOVECOT_ABI_VERSION; + +struct stats_user_module stats_user_module = + MODULE_CONTEXT_INIT(&mail_user_module_register); +struct stats_storage_module stats_storage_module = + MODULE_CONTEXT_INIT(&mail_storage_module_register); + +static struct stats_item *mail_stats_item; +static struct stats_connection *global_stats_conn = NULL; +static struct mail_user *stats_global_user = NULL; +static unsigned int stats_user_count = 0; + +static void session_stats_refresh_timeout(struct mail_user *user); + +static void stats_io_activate(struct mail_user *user) +{ + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user); + struct mail_stats *mail_stats; + + if (stats_user_count == 1) { + /* the first user sets the global user. the second user sets + it to NULL. when we get back to one user we'll need to set + the global user again somewhere. do it here. */ + stats_global_user = user; + /* skip time spent waiting in ioloop */ + mail_stats = stats_fill_ptr(suser->pre_io_stats, mail_stats_item); + mail_stats->clock_time = ioloop_timeval; + } else { + i_assert(stats_global_user == NULL); + + mail_user_stats_fill(user, suser->pre_io_stats); + } +} + +static void stats_add_session(struct mail_user *user) +{ + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user); + struct stats *new_stats, *diff_stats; + const char *error; + + new_stats = stats_alloc(pool_datastack_create()); + diff_stats = stats_alloc(pool_datastack_create()); + + mail_user_stats_fill(user, new_stats); + /* we'll count new_stats-pre_io_stats and add the changes to + session_stats. the new_stats can't be directly copied to + session_stats because there are some fields that don't start from + zero, like clock_time. (actually with stats_global_user code we're + requiring that clock_time is the only such field..) */ + if (!stats_diff(suser->pre_io_stats, new_stats, diff_stats, &error)) + i_error("stats: session stats shrank: %s", error); + stats_add(suser->session_stats, diff_stats); + /* copying is only needed if stats_global_user=NULL */ + stats_copy(suser->pre_io_stats, new_stats); +} + +static bool +session_stats_need_send(struct stats_user *suser, time_t now, + bool *changed_r, unsigned int *to_next_secs_r) +{ + unsigned int diff; + + *to_next_secs_r = SESSION_STATS_FORCE_REFRESH_SECS; + + if (stats_have_changed(suser->last_sent_session_stats, + suser->session_stats)) { + *to_next_secs_r = suser->refresh_secs; + *changed_r = TRUE; + return TRUE; + } + *changed_r = FALSE; + + diff = now - suser->last_session_update; + if (diff >= SESSION_STATS_FORCE_REFRESH_SECS) + return TRUE; + *to_next_secs_r = SESSION_STATS_FORCE_REFRESH_SECS - diff; + + if (!suser->session_sent_duplicate) { + if (suser->last_session_update != now) { + /* send one duplicate notification so stats reader + knows that this session is idle now */ + return TRUE; + } + } + return FALSE; +} + +static void session_stats_refresh(struct mail_user *user) +{ + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user); + unsigned int to_next_secs; + time_t now = time(NULL); + bool changed; + + if (!suser->stats_connected) { + if (mail_stats_connection_connect(suser->stats_conn, user) == 0) + suser->stats_connected = TRUE; + } + if (session_stats_need_send(suser, now, &changed, &to_next_secs) && + suser->stats_connected) { + suser->session_sent_duplicate = !changed; + suser->last_session_update = now; + stats_copy(suser->last_sent_session_stats, suser->session_stats); + mail_stats_connection_send_session(suser->stats_conn, user, + suser->session_stats); + } + + timeout_remove(&suser->to_stats_timeout); + suser->to_stats_timeout = + timeout_add(to_next_secs*1000, + session_stats_refresh_timeout, user); +} + +static struct mailbox_transaction_context * +stats_transaction_begin(struct mailbox *box, + enum mailbox_transaction_flags flags, + const char *reason) +{ + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(box->storage->user); + struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(box); + struct mailbox_transaction_context *trans; + struct stats_transaction_context *strans; + + trans = sbox->module_ctx.super.transaction_begin(box, flags, reason); + trans->stats_track = TRUE; + + strans = i_new(struct stats_transaction_context, 1); + strans->trans = trans; + DLLIST_PREPEND(&suser->transactions, strans); + + MODULE_CONTEXT_SET(trans, stats_storage_module, strans); + return trans; +} + +static void stats_transaction_free(struct stats_user *suser, + struct stats_transaction_context *strans) +{ + const struct mailbox_transaction_stats *src = &strans->trans->stats; + struct mailbox_transaction_stats *dest = + &suser->finished_transaction_stats; + + DLLIST_REMOVE(&suser->transactions, strans); + + dest->open_lookup_count += src->open_lookup_count; + dest->stat_lookup_count += src->stat_lookup_count; + dest->fstat_lookup_count += src->fstat_lookup_count; + dest->files_read_count += src->files_read_count; + dest->files_read_bytes += src->files_read_bytes; + dest->cache_hit_count += src->cache_hit_count; + i_free(strans); +} + +static int +stats_transaction_commit(struct mailbox_transaction_context *ctx, + struct mail_transaction_commit_changes *changes_r) +{ + struct stats_transaction_context *strans = STATS_CONTEXT_REQUIRE(ctx); + struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(ctx->box); + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(ctx->box->storage->user); + + stats_transaction_free(suser, strans); + return sbox->module_ctx.super.transaction_commit(ctx, changes_r); +} + +static void +stats_transaction_rollback(struct mailbox_transaction_context *ctx) +{ + struct stats_transaction_context *strans = STATS_CONTEXT_REQUIRE(ctx); + struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(ctx->box); + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(ctx->box->storage->user); + + stats_transaction_free(suser, strans); + sbox->module_ctx.super.transaction_rollback(ctx); +} + +static bool stats_search_next_nonblock(struct mail_search_context *ctx, + struct mail **mail_r, bool *tryagain_r) +{ + struct stats_mailbox *sbox = STATS_CONTEXT_REQUIRE(ctx->transaction->box); + struct mail_user *user = ctx->transaction->box->storage->user; + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user); + bool ret; + + ret = sbox->module_ctx.super. + search_next_nonblock(ctx, mail_r, tryagain_r); + if (!ret && !*tryagain_r) { + /* end of search */ + return FALSE; + } + + if (*tryagain_r || + ++suser->refresh_check_counter % REFRESH_CHECK_INTERVAL == 0) { + /* a) retrying, so this is a long running search. + b) we've returned enough matches */ + if (time(NULL) != suser->last_session_update) + session_stats_refresh(user); + } + return ret; +} + +static void +stats_notify_ok(struct mailbox *box, const char *text, void *context) +{ + struct stats_storage *sstorage = STATS_CONTEXT_REQUIRE(box->storage); + + /* most importantly we want to refresh stats for very long running + mailbox syncs */ + session_stats_refresh(box->storage->user); + + if (sstorage->old_callbacks.notify_ok != NULL) + sstorage->old_callbacks.notify_ok(box, text, context); +} + +static void stats_register_notify_callbacks(struct mail_storage *storage) +{ + struct stats_storage *sstorage = STATS_CONTEXT(storage); + + if (sstorage != NULL) + return; + + sstorage = p_new(storage->pool, struct stats_storage, 1); + sstorage->old_callbacks = storage->callbacks; + storage->callbacks.notify_ok = stats_notify_ok; + + MODULE_CONTEXT_SET(storage, stats_storage_module, sstorage); +} + +static void stats_mailbox_allocated(struct mailbox *box) +{ + struct mailbox_vfuncs *v = box->vlast; + struct stats_mailbox *sbox; + struct stats_user *suser = STATS_USER_CONTEXT(box->storage->user); + + if (suser == NULL) + return; + + stats_register_notify_callbacks(box->storage); + + sbox = p_new(box->pool, struct stats_mailbox, 1); + sbox->module_ctx.super = *v; + box->vlast = &sbox->module_ctx.super; + + v->transaction_begin = stats_transaction_begin; + v->transaction_commit = stats_transaction_commit; + v->transaction_rollback = stats_transaction_rollback; + v->search_next_nonblock = stats_search_next_nonblock; + MODULE_CONTEXT_SET(box, stats_storage_module, sbox); +} + +static void session_stats_refresh_timeout(struct mail_user *user) +{ + if (stats_global_user != NULL) + stats_add_session(user); + session_stats_refresh(user); +} + +static void stats_io_deactivate(struct mail_user *user) +{ + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user); + unsigned int last_update_secs; + + if (stats_global_user == NULL) + stats_add_session(user); + + last_update_secs = time(NULL) - suser->last_session_update; + if (last_update_secs >= suser->refresh_secs) { + if (stats_global_user != NULL) + stats_add_session(user); + session_stats_refresh(user); + } else if (suser->to_stats_timeout == NULL) { + suser->to_stats_timeout = + timeout_add(suser->refresh_secs*1000, + session_stats_refresh_timeout, user); + } +} + +static void stats_user_stats_fill(struct mail_user *user, struct stats *stats) +{ + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user); + struct mail_stats *mail_stats; + + mail_stats = stats_fill_ptr(stats, mail_stats_item); + mail_stats_fill(suser, mail_stats); + + suser->module_ctx.super.stats_fill(user, stats); +} + +static void stats_user_deinit(struct mail_user *user) +{ + struct stats_user *suser = STATS_USER_CONTEXT_REQUIRE(user); + struct stats_connection *stats_conn = suser->stats_conn; + + i_assert(stats_user_count > 0); + + stats_user_count--; + if (stats_global_user != NULL) { + /* we were updating the session lazily. do one final update. */ + i_assert(stats_global_user == user); + stats_add_session(user); + stats_global_user = NULL; + } + + io_loop_context_remove_callbacks(suser->ioloop_ctx, + stats_io_activate, + stats_io_deactivate, user); + /* send final stats before disconnection */ + session_stats_refresh(user); + if (suser->stats_connected) + mail_stats_connection_disconnect(stats_conn, user); + + timeout_remove(&suser->to_stats_timeout); + suser->module_ctx.super.deinit(user); + + stats_connection_unref(&stats_conn); +} + +static void stats_user_created(struct mail_user *user) +{ + struct ioloop_context *ioloop_ctx = + io_loop_get_current_context(current_ioloop); + struct stats_user *suser; + struct mail_user_vfuncs *v = user->vlast; + const char *path, *str, *error; + unsigned int refresh_secs; + + if (ioloop_ctx == NULL) { + /* we're probably running some test program, or at least + mail-storage-service wasn't used to create this user. + disable stats tracking. */ + return; + } + if (user->autocreated) { + /* lda / shared user. we're not tracking this one. */ + return; + } + + /* get refresh time */ + str = mail_user_plugin_getenv(user, "old_stats_refresh"); + if (str == NULL) + return; + if (settings_get_time(str, &refresh_secs, &error) < 0) { + i_error("stats: Invalid old_stats_refresh setting: %s", error); + return; + } + if (refresh_secs == 0) + return; + if (refresh_secs > SESSION_STATS_FORCE_REFRESH_SECS) { + i_warning("stats: stats_refresh too large, changing to %u", + SESSION_STATS_FORCE_REFRESH_SECS); + refresh_secs = SESSION_STATS_FORCE_REFRESH_SECS; + } + + if (global_stats_conn == NULL) { + path = mail_user_plugin_getenv(user, "old_stats_notify_path"); + if (path == NULL) + path = MAIL_STATS_FIFO_NAME; + if (path[0] != '/') + path = t_strconcat(user->set->base_dir, "/", path, NULL); + global_stats_conn = stats_connection_create(path); + } + stats_connection_ref(global_stats_conn); + + if (stats_user_count == 0) { + /* first user connection */ + stats_global_user = user; + } else if (stats_user_count == 1) { + /* second user connection. we'll need to start doing + per-io callback tracking now. (we might have been doing it + also previously but just temporarily quickly dropped to + having 1 user, in which case stats_global_user=NULL) */ + if (stats_global_user != NULL) { + stats_add_session(stats_global_user); + stats_global_user = NULL; + } + } + stats_user_count++; + + suser = p_new(user->pool, struct stats_user, 1); + suser->module_ctx.super = *v; + user->vlast = &suser->module_ctx.super; + v->deinit = stats_user_deinit; + v->stats_fill = stats_user_stats_fill; + + suser->refresh_secs = refresh_secs; + if (mail_user_plugin_getenv_bool(user, "old_stats_track_cmds")) + suser->track_commands = TRUE; + + suser->stats_conn = global_stats_conn; + if (user->session_id != NULL && user->session_id[0] != '\0') + suser->stats_session_id = user->session_id; + else { + guid_128_t guid; + + guid_128_generate(guid); + suser->stats_session_id = + p_strdup(user->pool, guid_128_to_string(guid)); + } + suser->last_session_update = time(NULL); + user->stats_enabled = TRUE; + + suser->ioloop_ctx = ioloop_ctx; + io_loop_context_add_callbacks(ioloop_ctx, + stats_io_activate, + stats_io_deactivate, user); + + suser->pre_io_stats = stats_alloc(user->pool); + suser->session_stats = stats_alloc(user->pool); + suser->last_sent_session_stats = stats_alloc(user->pool); + + MODULE_CONTEXT_SET(user, stats_user_module, suser); + if (mail_stats_connection_connect(suser->stats_conn, user) == 0) + suser->stats_connected = TRUE; + suser->to_stats_timeout = + timeout_add(suser->refresh_secs*1000, + session_stats_refresh_timeout, user); + /* fill the initial values. this is necessary for the process-global + values (e.g. getrusage()) if the process is reused for multiple + users. otherwise the next user will start with the previous one's + last values. */ + mail_user_stats_fill(user, suser->pre_io_stats); +} + +static struct mail_storage_hooks stats_mail_storage_hooks = { + .mailbox_allocated = stats_mailbox_allocated, + .mail_user_created = stats_user_created +}; + +void old_stats_plugin_init(struct module *module) +{ + mail_stats_item = stats_register(&mail_stats_vfuncs); + mail_storage_hooks_add(module, &stats_mail_storage_hooks); +} + +void old_stats_plugin_preinit(void) +{ + mail_stats_global_preinit(); +} + +void old_stats_plugin_deinit(void) +{ + if (global_stats_conn != NULL) + stats_connection_unref(&global_stats_conn); + mail_stats_fill_global_deinit(); + mail_storage_hooks_remove(&stats_mail_storage_hooks); + stats_unregister(&mail_stats_item); +} |