diff options
Diffstat (limited to 'src/lib-storage/mail-duplicate.c')
-rw-r--r-- | src/lib-storage/mail-duplicate.c | 767 |
1 files changed, 767 insertions, 0 deletions
diff --git a/src/lib-storage/mail-duplicate.c b/src/lib-storage/mail-duplicate.c new file mode 100644 index 0000000..7a78caa --- /dev/null +++ b/src/lib-storage/mail-duplicate.c @@ -0,0 +1,767 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "hex-binary.h" +#include "mkdir-parents.h" +#include "istream.h" +#include "ostream.h" +#include "time-util.h" +#include "home-expand.h" +#include "file-create-locked.h" +#include "file-dotlock.h" +#include "md5.h" +#include "hash.h" +#include "mail-user.h" +#include "mail-storage-settings.h" +#include "mail-duplicate.h" + +#include <fcntl.h> +#include <unistd.h> + +#define COMPRESS_PERCENTAGE 10 +#define DUPLICATE_BUFSIZE 4096 +#define DUPLICATE_VERSION 2 + +#define DUPLICATE_LOCK_FNAME_PREFIX "duplicate.lock." + +#define DUPLICATE_LOCK_TIMEOUT_SECS 65 +#define DUPLICATE_LOCK_WARN_SECS 4 +#define DUPLICATE_LOCK_MAX_LOCKS 100 + +enum mail_duplicate_lock_result { + MAIL_DUPLICATE_LOCK_OK, + MAIL_DUPLICATE_LOCK_IO_ERROR, + MAIL_DUPLICATE_LOCK_TIMEOUT, + MAIL_DUPLICATE_LOCK_TOO_MANY, + MAIL_DUPLICATE_LOCK_DEADLOCK, +}; + +struct mail_duplicate_lock { + int fd; + char *path; + struct file_lock *lock; + struct timeval start_time; +}; + +struct mail_duplicate { + const void *id; + unsigned int id_size; + + const char *user; + time_t time; + struct mail_duplicate_lock lock; + + bool marked:1; + bool changed:1; +}; + +struct mail_duplicate_file_header { + uint32_t version; +}; + +struct mail_duplicate_record_header { + uint32_t stamp; + uint32_t id_size; + uint32_t user_size; +}; + +struct mail_duplicate_transaction { + pool_t pool; + struct mail_duplicate_db *db; + ino_t db_ino; + struct event *event; + + HASH_TABLE(struct mail_duplicate *, struct mail_duplicate *) hash; + const char *path; + unsigned int id_lock_count; + + bool changed:1; +}; + +struct mail_duplicate_db { + struct mail_user *user; + struct event *event; + char *path; + char *lock_dir; + struct dotlock_settings dotlock_set; + + unsigned int transaction_count; +}; + +static const struct dotlock_settings default_mail_duplicate_dotlock_set = { + .timeout = 20, + .stale_timeout = 10, +}; + +static int +mail_duplicate_cmp(const struct mail_duplicate *d1, + const struct mail_duplicate *d2) +{ + return (d1->id_size == d2->id_size && + memcmp(d1->id, d2->id, d1->id_size) == 0 && + strcasecmp(d1->user, d2->user) == 0) ? 0 : 1; +} + +static unsigned int mail_duplicate_hash(const struct mail_duplicate *d) +{ + /* a char* hash function from ASU -- from glib */ + const unsigned char *s = d->id, *end = s + d->id_size; + unsigned int g, h = 0; + + while (s != end) { + h = (h << 4) + *s; + if ((g = h & 0xf0000000UL) != 0) { + h = h ^ (g >> 24); + h = h ^ g; + } + s++; + } + + return h ^ strcase_hash(d->user); +} + +static enum mail_duplicate_lock_result +duplicate_lock_failed(struct mail_duplicate_transaction *trans, + struct mail_duplicate *dup, const char *error) +{ + struct mail_duplicate_lock *lock = &dup->lock; + enum mail_duplicate_lock_result result; + int diff; + + i_assert(lock->fd == -1); + i_assert(lock->lock == NULL); + + if (errno == EDEADLK) { + /* deadlock */ + result = MAIL_DUPLICATE_LOCK_DEADLOCK; + } else if (errno != EAGAIN) { + /* not a lock timeout */ + result = MAIL_DUPLICATE_LOCK_IO_ERROR; + } else { + diff = timeval_diff_msecs(&ioloop_timeval, + &lock->start_time); + error = t_strdup_printf("Lock timeout in %d.%03d secs", + diff/1000, diff%1000); + result = MAIL_DUPLICATE_LOCK_TIMEOUT; + } + + e_error(trans->event, "Failed to lock %s: %s", lock->path, error); + i_free_and_null(lock->path); + i_zero(lock); + return result; +} + +static bool mail_duplicate_is_locked(struct mail_duplicate *dup) +{ + struct mail_duplicate_lock *lock = &dup->lock; + + return (lock->lock != NULL); +} + +static enum mail_duplicate_lock_result +mail_duplicate_lock(struct mail_duplicate_transaction *trans, + struct mail_duplicate *dup) +{ + struct file_create_settings lock_set = { + .lock_timeout_secs = DUPLICATE_LOCK_TIMEOUT_SECS, + .lock_settings = { + .lock_method = FILE_LOCK_METHOD_FCNTL, + .allow_deadlock = TRUE, + }, + }; + struct mail_duplicate_db *db = trans->db; + struct mail_duplicate_lock *lock = &dup->lock; + const char *error; + unsigned char id_md5[MD5_RESULTLEN]; + bool created; + int diff; + + if (mail_duplicate_is_locked(dup)) { + e_debug(trans->event, "Duplicate ID already locked"); + return MAIL_DUPLICATE_LOCK_OK; + } + if (trans->id_lock_count >= DUPLICATE_LOCK_MAX_LOCKS) { + e_debug(trans->event, "Too many duplicate IDs locked"); + return MAIL_DUPLICATE_LOCK_TOO_MANY; + } + + i_assert(db->lock_dir != NULL); + + lock->start_time = ioloop_timeval; + md5_get_digest(dup->id, dup->id_size, id_md5); + lock->path = i_strdup_printf("%s/"DUPLICATE_LOCK_FNAME_PREFIX"%s", + db->lock_dir, + binary_to_hex(id_md5, sizeof(id_md5))); + + e_debug(trans->event, "Lock duplicate ID (path=%s)", lock->path); + + lock->fd = file_create_locked(lock->path, &lock_set, &lock->lock, + &created, &error); + if (lock->fd == -1 && errno == ENOENT) { + /* parent directory missing - create it */ + if (mkdir_parents(db->lock_dir, 0700) < 0 && errno != EEXIST) { + error = t_strdup_printf( + "mkdir_parents(%s) failed: %m", db->lock_dir); + } else { + lock->fd = file_create_locked(lock->path, + &lock_set, &lock->lock, + &created, &error); + } + } + if (lock->fd == -1) + return duplicate_lock_failed(trans, dup, error); + + diff = timeval_diff_msecs(&ioloop_timeval, &lock->start_time); + if (diff >= (DUPLICATE_LOCK_WARN_SECS * 1000)) { + e_warning(trans->event, "Locking %s took %d.%03d secs", + lock->path, diff/1000, diff%1000); + } + + i_assert(mail_duplicate_is_locked(dup)); + trans->id_lock_count++; + return MAIL_DUPLICATE_LOCK_OK; +} + +static void +mail_duplicate_unlock(struct mail_duplicate_transaction *trans, + struct mail_duplicate *dup) +{ + int orig_errno = errno; + + if (dup->lock.path != NULL) { + struct mail_duplicate_lock *lock = &dup->lock; + + e_debug(trans->event, "Unlock duplicate ID (path=%s)", + lock->path); + i_unlink(lock->path); + file_lock_free(&lock->lock); + i_close_fd(&lock->fd); + i_free_and_null(lock->path); + i_zero(lock); + + i_assert(trans->id_lock_count > 0); + trans->id_lock_count--; + } + + errno = orig_errno; +} + +static int +mail_duplicate_read_records(struct mail_duplicate_transaction *trans, + struct istream *input, + unsigned int record_size) +{ + const unsigned char *data; + struct mail_duplicate_record_header hdr; + size_t size; + unsigned int change_count; + + change_count = 0; + while (i_stream_read_bytes(input, &data, &size, record_size) > 0) { + if (record_size == sizeof(hdr)) + memcpy(&hdr, data, sizeof(hdr)); + else { + /* FIXME: backwards compatibility with v1.0 */ + time_t stamp; + + i_assert(record_size == + sizeof(time_t) + sizeof(uint32_t)*2); + memcpy(&stamp, data, sizeof(stamp)); + hdr.stamp = stamp; + memcpy(&hdr.id_size, data + sizeof(time_t), + sizeof(hdr.id_size)); + memcpy(&hdr.user_size, + data + sizeof(time_t) + sizeof(uint32_t), + sizeof(hdr.user_size)); + } + i_stream_skip(input, record_size); + + if (hdr.id_size == 0 || hdr.user_size == 0 || + hdr.id_size > DUPLICATE_BUFSIZE || + hdr.user_size > DUPLICATE_BUFSIZE) { + e_error(trans->event, + "broken mail_duplicate file %s", trans->path); + return -1; + } + + if (i_stream_read_bytes(input, &data, &size, + hdr.id_size + hdr.user_size) <= 0) { + e_error(trans->event, + "unexpected end of file in %s", trans->path); + return -1; + } + + struct mail_duplicate dup_q, *dup; + + dup_q.id = data; + dup_q.id_size = hdr.id_size; + dup_q.user = t_strndup(data + hdr.id_size, hdr.user_size); + + dup = hash_table_lookup(trans->hash, &dup_q); + if ((time_t)hdr.stamp < ioloop_time) { + change_count++; + if (dup != NULL && !dup->changed) + dup->marked = FALSE; + } else { + if (dup == NULL) { + void *new_id; + + new_id = p_malloc(trans->pool, hdr.id_size); + memcpy(new_id, data, hdr.id_size); + + dup = p_new(trans->pool, + struct mail_duplicate, 1); + dup->id = new_id; + dup->id_size = hdr.id_size; + dup->user = p_strdup(trans->pool, dup_q.user); + hash_table_update(trans->hash, dup, dup); + } + if (!dup->changed) { + dup->marked = TRUE; + dup->time = hdr.stamp; + } + } + i_stream_skip(input, hdr.id_size + hdr.user_size); + } + + if (hash_table_count(trans->hash) * + COMPRESS_PERCENTAGE / 100 > change_count) + trans->changed = TRUE; + return 0; +} + +static int +mail_duplicate_read_db_from_fd(struct mail_duplicate_transaction *trans, int fd) +{ + struct istream *input; + struct mail_duplicate_file_header hdr; + const unsigned char *data; + size_t size; + struct stat st; + unsigned int record_size = 0; + + if (fstat(fd, &st) < 0) { + e_error(trans->event, + "stat(%s) failed: %m", trans->path); + return -1; + } + trans->db_ino = st.st_ino; + + /* <timestamp> <id_size> <user_size> <id> <user> */ + input = i_stream_create_fd(fd, DUPLICATE_BUFSIZE); + if (i_stream_read_bytes(input, &data, &size, sizeof(hdr)) > 0) { + memcpy(&hdr, data, sizeof(hdr)); + if (hdr.version == 0 || hdr.version > DUPLICATE_VERSION + 10) { + /* FIXME: backwards compatibility with v1.0 */ + record_size = sizeof(time_t) + sizeof(uint32_t)*2; + } else if (hdr.version == DUPLICATE_VERSION) { + record_size = sizeof(struct mail_duplicate_record_header); + i_stream_skip(input, sizeof(hdr)); + } + } + + if (record_size == 0) + i_unlink_if_exists(trans->path); + else T_BEGIN { + if (mail_duplicate_read_records(trans, input, record_size) < 0) + i_unlink_if_exists(trans->path); + } T_END; + + i_stream_unref(&input); + return 0; +} + +static int mail_duplicate_read_db_file(struct mail_duplicate_transaction *trans) +{ + int fd, ret; + + e_debug(trans->event, "Reading %s", trans->path); + + fd = open(trans->path, O_RDONLY); + if (fd == -1) { + if (errno == ENOENT) + return 0; + e_error(trans->event, + "open(%s) failed: %m", trans->path); + return -1; + } + + ret = mail_duplicate_read_db_from_fd(trans, fd); + + if (close(fd) < 0) { + e_error(trans->event, + "close(%s) failed: %m", trans->path); + } + return ret; +} + +static void mail_duplicate_read(struct mail_duplicate_transaction *trans) +{ + struct mail_duplicate_db *db = trans->db; + int new_fd; + struct dotlock *dotlock; + + new_fd = file_dotlock_open(&db->dotlock_set, trans->path, 0, &dotlock); + if (new_fd != -1) + ; + else if (errno != EAGAIN) { + e_error(trans->event, + "file_dotlock_open(%s) failed: %m", trans->path); + } else { + e_error(trans->event, + "Creating lock file for %s timed out in %u secs", + trans->path, db->dotlock_set.timeout); + } + + (void)mail_duplicate_read_db_file(trans); + + if (dotlock != NULL) + file_dotlock_delete(&dotlock); +} + +static void mail_duplicate_update(struct mail_duplicate_transaction *trans) +{ + struct stat st; + + if (stat(trans->path, &st) < 0) { + if (errno == ENOENT) { + e_debug(trans->event, "DB file not created yet"); + } else { + e_error(trans->event, + "stat(%s) failed: %m", trans->path); + } + } else if (trans->db_ino == st.st_ino) { + e_debug(trans->event, "DB file not changed"); + } else { + e_debug(trans->event, "DB file changed: " + "Updating duplicate records from DB file"); + + mail_duplicate_read(trans); + } +} + +struct mail_duplicate_transaction * +mail_duplicate_transaction_begin(struct mail_duplicate_db *db) +{ + struct mail_duplicate_transaction *trans; + pool_t pool; + + db->transaction_count++; + + pool = pool_alloconly_create("mail_duplicates", 10240); + + trans = p_new(pool, struct mail_duplicate_transaction, 1); + trans->pool = pool; + trans->db = db; + + trans->event = event_create(db->event); + event_set_append_log_prefix(trans->event, "transaction: "); + + if (db->path == NULL) { + /* Duplicate database disabled; return dummy transaction */ + e_debug(trans->event, "Transaction begin (dummy)"); + return trans; + } + + e_debug(trans->event, "Transaction begin; lock %s", db->path); + + trans->path = p_strdup(pool, db->path); + hash_table_create(&trans->hash, pool, 0, + mail_duplicate_hash, mail_duplicate_cmp); + + mail_duplicate_read(trans); + + return trans; +} + +static void +mail_duplicate_transaction_free(struct mail_duplicate_transaction **_trans) +{ + struct mail_duplicate_transaction *trans = *_trans; + struct hash_iterate_context *iter; + struct mail_duplicate *d; + + if (trans == NULL) + return; + *_trans = NULL; + + e_debug(trans->event, "Transaction free"); + + i_assert(trans->db->transaction_count > 0); + trans->db->transaction_count--; + + if (hash_table_is_created(trans->hash)) { + iter = hash_table_iterate_init(trans->hash); + while (hash_table_iterate(iter, trans->hash, &d, &d)) + mail_duplicate_unlock(trans, d); + hash_table_iterate_deinit(&iter); + hash_table_destroy(&trans->hash); + } + i_assert(trans->id_lock_count == 0); + + event_unref(&trans->event); + pool_unref(&trans->pool); +} + +static struct mail_duplicate * +mail_duplicate_get(struct mail_duplicate_transaction *trans, + const void *id, size_t id_size, const char *user) +{ + struct mail_duplicate dup_q, *dup; + + dup_q.id = id; + dup_q.id_size = id_size; + dup_q.user = user; + + dup = hash_table_lookup(trans->hash, &dup_q); + if (dup == NULL) { + dup = p_new(trans->pool, struct mail_duplicate, 1); + dup->id = p_memdup(trans->pool, id, id_size); + dup->id_size = id_size; + dup->user = p_strdup(trans->pool, user); + dup->time = (time_t)-1; + + hash_table_insert(trans->hash, dup, dup); + } + + return dup; +} + +enum mail_duplicate_check_result +mail_duplicate_check(struct mail_duplicate_transaction *trans, + const void *id, size_t id_size, const char *user) +{ + struct mail_duplicate *dup; + + if (trans->path == NULL) { + /* Duplicate database disabled */ + e_debug(trans->event, "Check ID (dummy)"); + return MAIL_DUPLICATE_CHECK_RESULT_NOT_FOUND; + } + + dup = mail_duplicate_get(trans, id, id_size, user); + + switch (mail_duplicate_lock(trans, dup)) { + case MAIL_DUPLICATE_LOCK_OK: + break; + case MAIL_DUPLICATE_LOCK_IO_ERROR: + e_debug(trans->event, + "Check ID: I/O error occurred while locking"); + return MAIL_DUPLICATE_CHECK_RESULT_IO_ERROR; + case MAIL_DUPLICATE_LOCK_TIMEOUT: + e_debug(trans->event, + "Check ID: lock timed out"); + return MAIL_DUPLICATE_CHECK_RESULT_LOCK_TIMEOUT; + case MAIL_DUPLICATE_LOCK_TOO_MANY: + e_debug(trans->event, + "Check ID: too many IDs locked"); + return MAIL_DUPLICATE_CHECK_RESULT_TOO_MANY_LOCKS; + case MAIL_DUPLICATE_LOCK_DEADLOCK: + e_debug(trans->event, + "Check ID: deadlock detected while locking"); + return MAIL_DUPLICATE_CHECK_RESULT_DEADLOCK; + } + + mail_duplicate_update(trans); + if (dup->marked) { + e_debug(trans->event, "Check ID: found"); + return MAIL_DUPLICATE_CHECK_RESULT_EXISTS; + } + + e_debug(trans->event, "Check ID: not found"); + return MAIL_DUPLICATE_CHECK_RESULT_NOT_FOUND; +} + +void mail_duplicate_mark(struct mail_duplicate_transaction *trans, + const void *id, size_t id_size, + const char *user, time_t timestamp) +{ + struct mail_duplicate *dup; + + if (trans->path == NULL) { + /* Duplicate database disabled */ + e_debug(trans->event, "Mark ID (dummy)"); + return; + } + + e_debug(trans->event, "Mark ID"); + + dup = mail_duplicate_get(trans, id, id_size, user); + + /* Must already be checked and locked */ + i_assert(mail_duplicate_is_locked(dup)); + + dup->time = timestamp; + dup->marked = TRUE; + dup->changed = TRUE; + + trans->changed = TRUE; +} + +void mail_duplicate_transaction_commit( + struct mail_duplicate_transaction **_trans) +{ + struct mail_duplicate_transaction *trans = *_trans; + struct mail_duplicate_file_header hdr; + struct mail_duplicate_record_header rec; + struct ostream *output; + struct hash_iterate_context *iter; + struct mail_duplicate *d; + int new_fd; + struct dotlock *dotlock; + + if (trans == NULL) + return; + *_trans = NULL; + + if (trans->path == NULL) { + e_debug(trans->event, "Commit (dummy)"); + mail_duplicate_transaction_free(&trans); + return; + } + if (!trans->changed) { + e_debug(trans->event, "Commit; no changes"); + mail_duplicate_transaction_free(&trans); + return; + } + + struct mail_duplicate_db *db = trans->db; + + i_assert(trans->path != NULL); + e_debug(trans->event, "Commit; overwrite %s", trans->path); + + new_fd = file_dotlock_open(&db->dotlock_set, trans->path, 0, &dotlock); + if (new_fd != -1) + ; + else if (errno != EAGAIN) { + e_error(trans->event, + "file_dotlock_open(%s) failed: %m", + trans->path); + mail_duplicate_transaction_free(&trans); + return; + } else { + e_error(trans->event, + "Creating lock file for %s timed out in %u secs", + trans->path, db->dotlock_set.timeout); + mail_duplicate_transaction_free(&trans); + return; + } + + i_zero(&hdr); + hdr.version = DUPLICATE_VERSION; + + output = o_stream_create_fd_file(new_fd, 0, FALSE); + o_stream_cork(output); + o_stream_nsend(output, &hdr, sizeof(hdr)); + + i_zero(&rec); + iter = hash_table_iterate_init(trans->hash); + while (hash_table_iterate(iter, trans->hash, &d, &d)) { + if (d->marked) { + rec.stamp = d->time; + rec.id_size = d->id_size; + rec.user_size = strlen(d->user); + + o_stream_nsend(output, &rec, sizeof(rec)); + o_stream_nsend(output, d->id, rec.id_size); + o_stream_nsend(output, d->user, rec.user_size); + } + } + hash_table_iterate_deinit(&iter); + + if (o_stream_finish(output) < 0) { + e_error(trans->event, "write(%s) failed: %s", + trans->path, o_stream_get_error(output)); + o_stream_unref(&output); + mail_duplicate_transaction_free(&trans); + return; + } + o_stream_unref(&output); + + if (file_dotlock_replace(&dotlock, 0) < 0) { + e_error(trans->event, + "file_dotlock_replace(%s) failed: %m", trans->path); + } + + iter = hash_table_iterate_init(trans->hash); + while (hash_table_iterate(iter, trans->hash, &d, &d)) + mail_duplicate_unlock(trans, d); + hash_table_iterate_deinit(&iter); + + mail_duplicate_transaction_free(&trans); +} + +void mail_duplicate_transaction_rollback( + struct mail_duplicate_transaction **_trans) +{ + struct mail_duplicate_transaction *trans = *_trans; + + if (trans == NULL) + return; + *_trans = NULL; + + if (trans->path == NULL) + e_debug(trans->event, "Rollback (dummy)"); + else + e_debug(trans->event, "Rollback"); + + mail_duplicate_transaction_free(&trans); +} + +struct mail_duplicate_db * +mail_duplicate_db_init(struct mail_user *user, const char *name) +{ + struct mail_duplicate_db *db; + const struct mail_storage_settings *mail_set; + const char *home = NULL; + const char *lock_dir; + + db = i_new(struct mail_duplicate_db, 1); + + db->event = event_create(user->event); + event_set_append_log_prefix(db->event, "duplicate db: "); + + e_debug(db->event, "Initialize"); + + db->user = user; + + if (mail_user_get_home(user, &home) <= 0) { + e_error(db->event, "User %s doesn't have home dir set, " + "disabling duplicate database", user->username); + return db; + } + + i_assert(home != NULL); + + db->path = i_strconcat(home, "/.dovecot.", name, NULL); + db->dotlock_set = default_mail_duplicate_dotlock_set; + + lock_dir = mail_user_get_volatile_dir(user); + if (lock_dir == NULL) + lock_dir = home; + db->lock_dir = i_strconcat(lock_dir, "/.dovecot.", name, ".locks", + NULL); + + mail_set = mail_user_set_get_storage_set(user); + db->dotlock_set.use_excl_lock = mail_set->dotlock_use_excl; + db->dotlock_set.nfs_flush = mail_set->mail_nfs_storage; + + return db; +} + +void mail_duplicate_db_deinit(struct mail_duplicate_db **_db) +{ + struct mail_duplicate_db *db = *_db; + + *_db = NULL; + + e_debug(db->event, "Cleanup"); + + i_assert(db->transaction_count == 0); + + event_unref(&db->event); + i_free(db->path); + i_free(db->lock_dir); + i_free(db); +} |