summaryrefslogtreecommitdiffstats
path: root/src/lib-index/mail-transaction-log.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/lib-index/mail-transaction-log.c664
1 files changed, 664 insertions, 0 deletions
diff --git a/src/lib-index/mail-transaction-log.c b/src/lib-index/mail-transaction-log.c
new file mode 100644
index 0000000..6e9b1eb
--- /dev/null
+++ b/src/lib-index/mail-transaction-log.c
@@ -0,0 +1,664 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "file-dotlock.h"
+#include "nfs-workarounds.h"
+#include "mmap-util.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/stat.h>
+
+static void
+mail_transaction_log_set_head(struct mail_transaction_log *log,
+ struct mail_transaction_log_file *file)
+{
+ i_assert(log->head != file);
+
+ file->refcount++;
+ log->head = file;
+
+ i_assert(log->files != NULL);
+ i_assert(log->files->next != NULL || log->files == file);
+}
+
+struct mail_transaction_log *
+mail_transaction_log_alloc(struct mail_index *index)
+{
+ struct mail_transaction_log *log;
+
+ log = i_new(struct mail_transaction_log, 1);
+ log->index = index;
+ return log;
+}
+
+static void mail_transaction_log_2_unlink_old(struct mail_transaction_log *log)
+{
+ struct stat st;
+ uint32_t log2_rotate_time = log->index->map->hdr.log2_rotate_time;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index))
+ return;
+
+ if (log2_rotate_time == 0) {
+ if (nfs_safe_stat(log->filepath2, &st) == 0)
+ log2_rotate_time = st.st_mtime;
+ else if (errno == ENOENT)
+ log2_rotate_time = (uint32_t)-1;
+ else {
+ mail_index_set_error(log->index,
+ "stat(%s) failed: %m", log->filepath2);
+ return;
+ }
+ }
+
+ if (log2_rotate_time != (uint32_t)-1 &&
+ ioloop_time - (time_t)log2_rotate_time >= (time_t)log->index->optimization_set.log.log2_max_age_secs &&
+ !log->index->readonly) {
+ i_unlink_if_exists(log->filepath2);
+ log2_rotate_time = (uint32_t)-1;
+ }
+
+ if (log2_rotate_time != log->index->map->hdr.log2_rotate_time) {
+ /* Either the log2_rotate_time in header was missing, or we
+ just deleted the .log.2 and need to set it as nonexistent.
+ Either way we need to update the header.
+
+ Write this as part of the next sync's transaction. We're
+ here because we're already opening a sync lock, so it'll
+ always happen. It's also required especially with mdbox map
+ index, which doesn't like changes done outside syncing. */
+ log->index->hdr_log2_rotate_time_delayed_update =
+ log2_rotate_time;
+ }
+}
+
+int mail_transaction_log_open(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+ const char *reason;
+ int ret;
+
+ i_free(log->filepath);
+ i_free(log->filepath2);
+ log->filepath = i_strconcat(log->index->filepath,
+ MAIL_TRANSACTION_LOG_SUFFIX, NULL);
+ log->filepath2 = i_strconcat(log->filepath, ".2", NULL);
+
+ if (log->open_file != NULL)
+ mail_transaction_log_file_free(&log->open_file);
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index))
+ return 0;
+
+ file = mail_transaction_log_file_alloc(log, log->filepath);
+ if ((ret = mail_transaction_log_file_open(file, &reason)) <= 0) {
+ /* leave the file for _create() */
+ log->open_file = file;
+ return ret;
+ }
+ mail_transaction_log_set_head(log, file);
+ return 1;
+}
+
+int mail_transaction_log_create(struct mail_transaction_log *log, bool reset)
+{
+ struct mail_transaction_log_file *file;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index)) {
+ file = mail_transaction_log_file_alloc_in_memory(log);
+ mail_transaction_log_set_head(log, file);
+ return 0;
+ }
+
+ file = mail_transaction_log_file_alloc(log, log->filepath);
+ if (log->open_file != NULL) {
+ /* remember what file we tried to open. if someone else created
+ a new file, use it instead of recreating it */
+ file->st_ino = log->open_file->st_ino;
+ file->st_dev = log->open_file->st_dev;
+ file->last_size = log->open_file->last_size;
+ file->last_mtime = log->open_file->last_mtime;
+ mail_transaction_log_file_free(&log->open_file);
+ }
+
+ if (mail_transaction_log_file_create(file, reset) < 0) {
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+
+ mail_transaction_log_set_head(log, file);
+ return 1;
+}
+
+void mail_transaction_log_close(struct mail_transaction_log *log)
+{
+ i_assert(log->views == NULL);
+
+ if (log->open_file != NULL)
+ mail_transaction_log_file_free(&log->open_file);
+ if (log->head != NULL)
+ log->head->refcount--;
+ mail_transaction_logs_clean(log);
+ i_assert(log->files == NULL);
+}
+
+void mail_transaction_log_free(struct mail_transaction_log **_log)
+{
+ struct mail_transaction_log *log = *_log;
+
+ *_log = NULL;
+
+ mail_transaction_log_close(log);
+ log->index->log = NULL;
+ i_free(log->filepath);
+ i_free(log->filepath2);
+ i_free(log);
+}
+
+int mail_transaction_log_move_to_memory(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+
+ if (!log->index->initial_mapped && log->files != NULL &&
+ log->files->hdr.prev_file_seq != 0) {
+ /* we couldn't read dovecot.index and we don't have the first
+ .log file, so just start from scratch */
+ mail_transaction_log_close(log);
+ }
+
+ i_free(log->filepath);
+ i_free(log->filepath2);
+ log->filepath = i_strconcat(log->index->filepath,
+ MAIL_TRANSACTION_LOG_SUFFIX, NULL);
+ log->filepath2 = i_strconcat(log->filepath, ".2", NULL);
+
+ if (log->head != NULL)
+ return mail_transaction_log_file_move_to_memory(log->head);
+ else {
+ file = mail_transaction_log_file_alloc_in_memory(log);
+ mail_transaction_log_set_head(log, file);
+ return 0;
+ }
+}
+
+void mail_transaction_log_indexid_changed(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+
+ mail_transaction_logs_clean(log);
+
+ for (file = log->files; file != NULL; file = file->next) {
+ if (file->hdr.indexid != log->index->indexid) {
+ mail_transaction_log_file_set_corrupted(file,
+ "indexid changed: %u -> %u",
+ file->hdr.indexid, log->index->indexid);
+ }
+ }
+
+ if (log->head != NULL &&
+ log->head->hdr.indexid != log->index->indexid) {
+ struct mail_transaction_log_file *old_head = log->head;
+
+ (void)mail_transaction_log_create(log, FALSE);
+ if (--old_head->refcount == 0) {
+ if (old_head == log->head) {
+ /* failed to create a new log */
+ log->head = NULL;
+ }
+ mail_transaction_log_file_free(&old_head);
+ }
+ }
+}
+
+void mail_transaction_logs_clean(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file, *next;
+
+ /* remove only files from the beginning. this way if a view has
+ referenced an old file, it can still find the new files even if
+ there aren't any references to it currently. */
+ for (file = log->files; file != NULL; file = next) {
+ next = file->next;
+
+ i_assert(file->refcount >= 0);
+ if (file->refcount > 0)
+ break;
+
+ mail_transaction_log_file_free(&file);
+ }
+ /* sanity check: we shouldn't have locked refcount=0 files */
+ for (; file != NULL; file = file->next) {
+ i_assert(!file->locked || file->refcount > 0);
+ }
+ i_assert(log->head == NULL || log->files != NULL);
+}
+
+bool mail_transaction_log_want_rotate(struct mail_transaction_log *log,
+ const char **reason_r)
+{
+ struct mail_transaction_log_file *file = log->head;
+
+ if (file->need_rotate != NULL) {
+ *reason_r = t_strdup(file->need_rotate);
+ return TRUE;
+ }
+
+ if (file->hdr.major_version < MAIL_TRANSACTION_LOG_MAJOR_VERSION ||
+ (file->hdr.major_version == MAIL_TRANSACTION_LOG_MAJOR_VERSION &&
+ file->hdr.minor_version < MAIL_TRANSACTION_LOG_MINOR_VERSION)) {
+ /* upgrade immediately to a new log file format */
+ *reason_r = t_strdup_printf(
+ ".log file format version %u.%u is too old",
+ file->hdr.major_version, file->hdr.minor_version);
+ return TRUE;
+ }
+
+ if (file->sync_offset > log->index->optimization_set.log.max_size) {
+ /* file is too large, definitely rotate */
+ *reason_r = t_strdup_printf(
+ ".log file size %"PRIuUOFF_T" > max_size %"PRIuUOFF_T,
+ file->sync_offset, log->index->optimization_set.log.max_size);
+ return TRUE;
+ }
+ if (file->sync_offset < log->index->optimization_set.log.min_size) {
+ /* file is still too small */
+ return FALSE;
+ }
+ /* rotate if the timestamp is old enough */
+ if (file->hdr.create_stamp <
+ ioloop_time - log->index->optimization_set.log.min_age_secs) {
+ *reason_r = t_strdup_printf(
+ ".log create_stamp %u is older than %u secs",
+ file->hdr.create_stamp,
+ log->index->optimization_set.log.min_age_secs);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int mail_transaction_log_rotate(struct mail_transaction_log *log, bool reset)
+{
+ struct mail_transaction_log_file *file, *old_head;
+ const char *path = log->head->filepath;
+ struct stat st;
+ int ret;
+
+ i_assert(log->head->locked);
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index)) {
+ file = mail_transaction_log_file_alloc_in_memory(log);
+ if (reset) {
+ file->hdr.prev_file_seq = 0;
+ file->hdr.prev_file_offset = 0;
+ }
+ } else {
+ /* we're locked, we shouldn't need to worry about ESTALE
+ problems in here. */
+ if (fstat(log->head->fd, &st) < 0) {
+ mail_index_file_set_syscall_error(log->index,
+ log->head->filepath, "fstat()");
+ return -1;
+ }
+
+ file = mail_transaction_log_file_alloc(log, path);
+
+ file->st_dev = st.st_dev;
+ file->st_ino = st.st_ino;
+ file->last_mtime = st.st_mtime;
+ file->last_size = st.st_size;
+
+ if ((ret = mail_transaction_log_file_create(file, reset)) < 0) {
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+ if (ret == 0) {
+ mail_index_set_error(log->index,
+ "Transaction log %s was recreated while we had it locked - "
+ "locking is broken (lock_method=%s)", path,
+ file_lock_method_to_str(log->index->set.lock_method));
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+ i_assert(file->locked);
+ }
+
+ old_head = log->head;
+ mail_transaction_log_set_head(log, file);
+
+ e_debug(log->index->event, "Rotated transaction log %s (seq=%u, reset=%s)",
+ file->filepath, file->hdr.file_seq, reset ? "yes" : "no");
+
+ /* the newly created log file is already locked */
+ mail_transaction_log_file_unlock(old_head,
+ !log->index->log_sync_locked ? "rotating" :
+ "rotating while syncing");
+ if (--old_head->refcount == 0)
+ mail_transaction_logs_clean(log);
+ return 0;
+}
+
+static int
+mail_transaction_log_refresh(struct mail_transaction_log *log, bool nfs_flush,
+ const char **reason_r)
+{
+ struct mail_transaction_log_file *file;
+ struct stat st;
+
+ i_assert(log->head != NULL);
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(log->head)) {
+ *reason_r = "Log is in memory";
+ return 0;
+ }
+
+ if (nfs_flush &&
+ (log->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0)
+ nfs_flush_file_handle_cache(log->filepath);
+ if (nfs_safe_stat(log->filepath, &st) < 0) {
+ if (errno != ENOENT) {
+ mail_index_file_set_syscall_error(log->index,
+ log->filepath,
+ "stat()");
+ *reason_r = t_strdup_printf("stat(%s) failed: %m", log->filepath);
+ return -1;
+ }
+ /* We shouldn't lose dovecot.index.log unless the mailbox was
+ deleted or renamed. Just fail this and let the mailbox
+ opening code figure out whether to create a new log file
+ or not. Anything else can cause unwanted behavior (e.g.
+ mailbox deletion not fully finishing due to .nfs* files and
+ an IDLEing IMAP process creating the index back here). */
+ log->index->index_deleted = TRUE;
+ *reason_r = "Trasnaction log lost while it was open";
+ return -1;
+ } else if (log->head->st_ino == st.st_ino &&
+ CMP_DEV_T(log->head->st_dev, st.st_dev)) {
+ /* NFS: log files get rotated to .log.2 files instead
+ of being unlinked, so we don't bother checking if
+ the existing file has already been unlinked here
+ (in which case inodes could match but point to
+ different files) */
+ *reason_r = "Log inode is unchanged";
+ return 0;
+ }
+
+ file = mail_transaction_log_file_alloc(log, log->filepath);
+ if (mail_transaction_log_file_open(file, reason_r) <= 0) {
+ *reason_r = t_strdup_printf(
+ "Failed to refresh main transaction log: %s", *reason_r);
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+
+ i_assert(!file->locked);
+
+ struct mail_transaction_log_file *old_head = log->head;
+ mail_transaction_log_set_head(log, file);
+ if (--old_head->refcount == 0)
+ mail_transaction_logs_clean(log);
+ *reason_r = "Log reopened";
+ return 0;
+}
+
+void mail_transaction_log_get_mailbox_sync_pos(struct mail_transaction_log *log,
+ uint32_t *file_seq_r,
+ uoff_t *file_offset_r)
+{
+ *file_seq_r = log->head->hdr.file_seq;
+ *file_offset_r = log->head->max_tail_offset;
+}
+
+void mail_transaction_log_set_mailbox_sync_pos(struct mail_transaction_log *log,
+ uint32_t file_seq,
+ uoff_t file_offset)
+{
+ i_assert(file_seq == log->head->hdr.file_seq);
+ i_assert(file_offset >= log->head->last_read_hdr_tail_offset);
+
+ if (file_offset >= log->head->max_tail_offset)
+ log->head->max_tail_offset = file_offset;
+}
+
+int mail_transaction_log_find_file(struct mail_transaction_log *log,
+ uint32_t file_seq, bool nfs_flush,
+ struct mail_transaction_log_file **file_r,
+ const char **reason_r)
+{
+ struct mail_transaction_log_file *file;
+ const char *reason;
+ int ret;
+
+ if (file_seq > log->head->hdr.file_seq) {
+ /* see if the .log file has been recreated */
+ if (log->head->locked) {
+ /* transaction log is locked. there's no way a newer
+ file exists. */
+ *reason_r = "Log is locked - newer log can't exist";
+ return 0;
+ }
+
+ if (mail_transaction_log_refresh(log, FALSE, &reason) < 0) {
+ *reason_r = reason;
+ return -1;
+ }
+ if (file_seq > log->head->hdr.file_seq) {
+ if (!nfs_flush ||
+ (log->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) == 0) {
+ *reason_r = t_strdup_printf(
+ "Requested newer log than exists: %s", reason);
+ return 0;
+ }
+ /* try again, this time flush attribute cache */
+ if (mail_transaction_log_refresh(log, TRUE, &reason) < 0) {
+ *reason_r = t_strdup_printf(
+ "Log refresh with NFS flush failed: %s", reason);
+ return -1;
+ }
+ if (file_seq > log->head->hdr.file_seq) {
+ *reason_r = t_strdup_printf(
+ "Requested newer log than exists - "
+ "still after NFS flush: %s", reason);
+ return 0;
+ }
+ }
+ }
+
+ for (file = log->files; file != NULL; file = file->next) {
+ if (file->hdr.file_seq == file_seq) {
+ *file_r = file;
+ return 1;
+ }
+ if (file->hdr.file_seq > file_seq &&
+ file->hdr.prev_file_seq == 0) {
+ /* Fail here mainly to avoid unnecessarily trying to
+ open .log.2 that most likely doesn't even exist. */
+ *reason_r = "Log was reset after requested file_seq";
+ return 0;
+ }
+ }
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index)) {
+ *reason_r = "Logs are only in memory";
+ return 0;
+ }
+
+ /* see if we have it in log.2 file */
+ file = mail_transaction_log_file_alloc(log, log->filepath2);
+ if ((ret = mail_transaction_log_file_open(file, reason_r)) <= 0) {
+ *reason_r = t_strdup_printf(
+ "Not found from .log.2: %s", *reason_r);
+ mail_transaction_log_file_free(&file);
+ return ret;
+ }
+
+ /* but is it what we expected? */
+ if (file->hdr.file_seq != file_seq) {
+ *reason_r = t_strdup_printf(".log.2 contains file_seq=%u",
+ file->hdr.file_seq);
+ return 0;
+ }
+
+ *file_r = file;
+ return 1;
+}
+
+int mail_transaction_log_lock_head(struct mail_transaction_log *log,
+ const char *lock_reason)
+{
+ struct mail_transaction_log_file *file;
+ time_t lock_wait_started, lock_secs = 0;
+ const char *reason;
+ int ret = 0;
+
+ /* we want to get the head file locked. this is a bit racy,
+ since by the time we have it locked a new log file may have been
+ created.
+
+ creating new log file requires locking the head file, so if we
+ can lock it and don't see another file, we can be sure no-one is
+ creating a new log at the moment */
+
+ lock_wait_started = time(NULL);
+ for (;;) {
+ file = log->head;
+ if (mail_transaction_log_file_lock(file) < 0)
+ return -1;
+
+ file->refcount++;
+ ret = mail_transaction_log_refresh(log, TRUE, &reason);
+ if (--file->refcount == 0) {
+ mail_transaction_log_file_unlock(file, t_strdup_printf(
+ "trying to lock head for %s", lock_reason));
+ mail_transaction_logs_clean(log);
+ file = NULL;
+ }
+
+ if (ret == 0 && log->head == file) {
+ /* success */
+ i_assert(file != NULL);
+ lock_secs = file->lock_create_time - lock_wait_started;
+ break;
+ }
+
+ if (file != NULL) {
+ mail_transaction_log_file_unlock(file, t_strdup_printf(
+ "trying to lock head for %s", lock_reason));
+ }
+ if (ret < 0)
+ break;
+
+ /* try again */
+ }
+ if (lock_secs > MAIL_TRANSACTION_LOG_LOCK_WARN_SECS) {
+ i_warning("Locking transaction log file %s took %ld seconds (%s)",
+ log->head->filepath, (long)lock_secs, lock_reason);
+ }
+
+ i_assert(ret < 0 || log->head != NULL);
+ return ret;
+}
+
+int mail_transaction_log_sync_lock(struct mail_transaction_log *log,
+ const char *lock_reason,
+ uint32_t *file_seq_r, uoff_t *file_offset_r)
+{
+ const char *reason;
+
+ i_assert(!log->index->log_sync_locked);
+
+ if (!log->log_2_unlink_checked) {
+ /* we need to check once in a while if .log.2 should be deleted
+ to avoid wasting space on such old files. but we also don't
+ want to waste time on checking it when the same mailbox
+ gets opened over and over again rapidly (e.g. pop3). so
+ do this only when there have actually been some changes
+ to mailbox (i.e. when it's being locked here) */
+ log->log_2_unlink_checked = TRUE;
+ mail_transaction_log_2_unlink_old(log);
+ }
+
+ if (mail_transaction_log_lock_head(log, lock_reason) < 0)
+ return -1;
+
+ /* update sync_offset */
+ if (mail_transaction_log_file_map(log->head, log->head->sync_offset,
+ UOFF_T_MAX, &reason) <= 0) {
+ mail_index_set_error(log->index,
+ "Failed to map transaction log %s at "
+ "sync_offset=%"PRIuUOFF_T" after locking: %s",
+ log->head->filepath, log->head->sync_offset, reason);
+ mail_transaction_log_file_unlock(log->head, t_strdup_printf(
+ "%s - map failed", lock_reason));
+ return -1;
+ }
+
+ log->index->log_sync_locked = TRUE;
+ *file_seq_r = log->head->hdr.file_seq;
+ *file_offset_r = log->head->sync_offset;
+ return 0;
+}
+
+void mail_transaction_log_sync_unlock(struct mail_transaction_log *log,
+ const char *lock_reason)
+{
+ i_assert(log->index->log_sync_locked);
+
+ log->index->log_sync_locked = FALSE;
+ mail_transaction_log_file_unlock(log->head, lock_reason);
+}
+
+void mail_transaction_log_get_head(struct mail_transaction_log *log,
+ uint32_t *file_seq_r, uoff_t *file_offset_r)
+{
+ *file_seq_r = log->head->hdr.file_seq;
+ *file_offset_r = log->head->sync_offset;
+}
+
+void mail_transaction_log_get_tail(struct mail_transaction_log *log,
+ uint32_t *file_seq_r)
+{
+ struct mail_transaction_log_file *tail, *file = log->files;
+
+ for (tail = file; file->next != NULL; file = file->next) {
+ if (file->hdr.file_seq + 1 != file->next->hdr.file_seq)
+ tail = file->next;
+ }
+ *file_seq_r = tail->hdr.file_seq;
+}
+
+bool mail_transaction_log_is_head_prev(struct mail_transaction_log *log,
+ uint32_t file_seq, uoff_t file_offset)
+{
+ return log->head->hdr.prev_file_seq == file_seq &&
+ log->head->hdr.prev_file_offset == file_offset;
+}
+
+int mail_transaction_log_unlink(struct mail_transaction_log *log)
+{
+ if (unlink(log->filepath) < 0 &&
+ errno != ENOENT && errno != ESTALE) {
+ mail_index_file_set_syscall_error(log->index, log->filepath,
+ "unlink()");
+ return -1;
+ }
+ return 0;
+}
+
+void mail_transaction_log_get_dotlock_set(struct mail_transaction_log *log,
+ struct dotlock_settings *set_r)
+{
+ struct mail_index *index = log->index;
+
+ i_zero(set_r);
+ set_r->timeout = I_MIN(MAIL_TRANSACTION_LOG_LOCK_TIMEOUT,
+ index->set.max_lock_timeout_secs);
+ set_r->stale_timeout = MAIL_TRANSACTION_LOG_DOTLOCK_CHANGE_TIMEOUT;
+ set_r->nfs_flush = (index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0;
+ set_r->use_excl_lock =
+ (index->flags & MAIL_INDEX_OPEN_FLAG_DOTLOCK_USE_EXCL) != 0;
+}