summaryrefslogtreecommitdiffstats
path: root/src/lib-index/test-mail-cache-purge.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib-index/test-mail-cache-purge.c')
-rw-r--r--src/lib-index/test-mail-cache-purge.c1076
1 files changed, 1076 insertions, 0 deletions
diff --git a/src/lib-index/test-mail-cache-purge.c b/src/lib-index/test-mail-cache-purge.c
new file mode 100644
index 0000000..525754b
--- /dev/null
+++ b/src/lib-index/test-mail-cache-purge.c
@@ -0,0 +1,1076 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "test-common.h"
+#include "test-mail-cache.h"
+
+#include <stdio.h>
+#include <sys/wait.h>
+
+static void test_mail_cache_read_during_purge2(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ string_t *str = t_str_new(16);
+
+ i_set_failure_prefix("index2: ");
+
+ /* read from cache via 2nd index */
+ test_mail_cache_init(test_mail_index_open(), &ctx);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(strcmp(str_c(str), "foo1") == 0);
+ mail_cache_view_close(&cache_view);
+
+ test_mail_cache_deinit(&ctx);
+}
+
+static void test_mail_cache_read_during_purge(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_transaction *trans;
+ int status;
+
+ test_begin("mail cache read during purge");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* lock the index for cache purge */
+ uint32_t log_seq;
+ uoff_t log_offset;
+ test_assert(mail_transaction_log_sync_lock(ctx.index->log, "purge", &log_seq, &log_offset) == 0);
+
+ /* start purging cache using the 1st index, but don't commit yet */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ test_assert(mail_cache_purge_with_trans(ctx.cache, trans, (uint32_t)-1, "test") == 0);
+
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ test_mail_cache_read_during_purge2();
+ /* cleanup so valgrind doesn't complain about memory leaks */
+ mail_index_transaction_rollback(&trans);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ test_mail_cache_deinit(&ctx);
+ test_exit(test_has_failed() ? 10 : 0);
+ default:
+ break;
+ }
+
+ /* Wait a bit to make sure the child function has had a chance to run.
+ It's supposed to be waiting on the locked .log file. */
+ usleep(100000);
+ /* finish cache purging */
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ mail_index_view_close(&ctx.view);
+
+ /* wait for child to finish execution */
+ if (wait(&status) == -1)
+ i_error("wait() failed: %m");
+ test_assert(status == 0);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_write_during_purge2(void)
+{
+ struct test_mail_cache_ctx ctx;
+
+ i_set_failure_prefix("index2: ");
+
+ /* add to cache via 2nd index */
+ test_mail_cache_init(test_mail_index_open(), &ctx);
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar2");
+ test_mail_cache_deinit(&ctx);
+}
+
+static void test_mail_cache_write_during_purge(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_view *view;
+ struct mail_cache_view *cache_view;
+ struct mail_index_transaction *trans;
+ string_t *str = t_str_new(16);
+ int status;
+
+ test_begin("mail cache write during purge");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* lock the index for cache purge */
+ uint32_t log_seq;
+ uoff_t log_offset;
+ test_assert(mail_transaction_log_sync_lock(ctx.index->log, "purge", &log_seq, &log_offset) == 0);
+
+ /* start purging cache using the 1st index, but don't commit yet */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ test_assert(mail_cache_purge_with_trans(ctx.cache, trans, (uint32_t)-1, "test") == 0);
+
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ test_mail_cache_write_during_purge2();
+ /* cleanup so valgrind doesn't complain about memory leaks */
+ mail_index_transaction_rollback(&trans);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ test_mail_cache_deinit(&ctx);
+ test_exit(test_has_failed() ? 10 : 0);
+ default:
+ break;
+ }
+
+ /* Wait a bit to make sure the child function has had a chance to run.
+ It's supposed to be waiting on the locked .log file. */
+ usleep(100000);
+ /* finish cache purge */
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ mail_index_view_close(&ctx.view);
+
+ /* wait for child to finish execution */
+ if (wait(&status) == -1)
+ i_error("wait() failed: %m");
+ test_assert(status == 0);
+
+ /* make sure both cache fields are visible */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+
+ view = mail_index_view_open(ctx.index);
+ cache_view = mail_cache_view_open(ctx.cache, view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(strcmp(str_c(str), "foo1") == 0);
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field2.idx) == 1);
+ test_assert(strcmp(str_c(str), "bar2") == 0);
+ mail_cache_view_close(&cache_view);
+ mail_index_view_close(&view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_purge_while_cache_locked(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ string_t *str = t_str_new(16);
+ int status;
+
+ test_begin("mail cache purge while cache locked");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* lock the cache */
+ test_assert(mail_cache_lock(ctx.cache) == 1);
+
+ /* purge the cache in another process */
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ test_mail_cache_purge();
+ test_mail_cache_deinit(&ctx);
+ test_exit(test_has_failed() ? 10 : 0);
+ default:
+ break;
+ }
+
+ /* Wait a bit to make sure the child function has had a chance to run.
+ It should start purging, which would wait for our cache lock. */
+ usleep(100000);
+
+ mail_cache_unlock(ctx.cache);
+
+ /* wait for child to finish execution */
+ if (wait(&status) == -1)
+ i_error("wait() failed: %m");
+ test_assert(status == 0);
+
+ /* make sure the cache is still usable */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+ test_mail_cache_view_sync(&ctx);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(strcmp(str_c(str), "foo1") == 0);
+ mail_cache_view_close(&cache_view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static bool cache_equals(struct mail_cache_view *cache_view, uint32_t seq,
+ unsigned int field_idx, const char *value)
+{
+ string_t *str = str_new(default_pool, 128);
+ int ret = mail_cache_lookup_field(cache_view, str, seq, field_idx);
+ bool match;
+
+ if (value != NULL) {
+ test_assert_idx(ret == 1, seq);
+ match = strcmp(str_c(str), value) == 0;
+ test_assert_idx(match, seq);
+ } else {
+ test_assert_idx(ret == 0, seq);
+ match = ret == 0;
+ }
+ str_free(&str);
+ return match;
+}
+
+static void test_mail_cache_purge_during_write_n(unsigned int num_mails,
+ bool commit_saves)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .record_max_size = 1024*1024,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_view *updated_view;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ struct mail_index_transaction *trans;
+ uint32_t seq;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ /* Add mails */
+ test_mail_cache_add_mail(&ctx, UINT_MAX, "");
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ for (seq = 2; seq <= num_mails; seq++)
+ mail_index_append(trans, seq, &seq);
+
+ if (commit_saves) {
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_mail_cache_view_sync(&ctx);
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ }
+ /* start adding a small cached field to mail1 */
+ updated_view = mail_index_transaction_open_updated_view(trans);
+ cache_view = mail_cache_view_open(ctx.cache, updated_view);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ mail_cache_add(cache_trans, 1, ctx.cache_field.idx, "foo1", 4);
+
+ /* add a huge field to mail2, which triggers flushing */
+ size_t huge_field_size = MAIL_CACHE_MAX_WRITE_BUFFER + 1024;
+ char *huge_field = i_malloc(huge_field_size + 1);
+ memset(huge_field, 'x', huge_field_size);
+ mail_cache_add(cache_trans, 2, ctx.cache_field.idx,
+ huge_field, huge_field_size);
+
+ /* verify that cached fields are still accessible */
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+
+ /* purge using a 2nd index */
+ test_mail_cache_purge();
+
+ if (num_mails == 2) {
+ /* the mails are still accessible after purge */
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ } else if (!commit_saves) {
+ /* add 3rd mail, which attempts to flush 2nd mail and finds
+ that the first mail is already lost */
+ test_expect_error_string("Purging lost 1 written cache records");
+ mail_cache_add(cache_trans, 3, ctx.cache_field.idx, "foo3", 4);
+ test_expect_no_more_errors();
+
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ test_assert(cache_equals(cache_view, 3, ctx.cache_field.idx, "foo3"));
+ } else {
+ /* add 3rd mail, which commits the first two mails */
+ mail_cache_add(cache_trans, 3, ctx.cache_field.idx, "foo3", 4);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ test_assert(cache_equals(cache_view, 3, ctx.cache_field.idx, "foo3"));
+ }
+
+ /* finish committing cached fields */
+ if (num_mails == 2 && !commit_saves)
+ test_expect_error_string("Purging lost 1 written cache records");
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_expect_no_more_errors();
+ mail_index_view_close(&updated_view);
+ mail_cache_view_close(&cache_view);
+
+ /* see that we lost the first flush without commit_saves, but not the others */
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ if (commit_saves)
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ else
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ if (num_mails >= 3)
+ test_assert(cache_equals(cache_view, 3, ctx.cache_field.idx, "foo3"));
+ mail_cache_view_close(&cache_view);
+
+ mail_index_view_close(&ctx.view);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ i_free(huge_field);
+}
+
+static void test_mail_cache_write_lost_during_purge(void)
+{
+ test_begin("mail cache write lost during purge");
+ test_mail_cache_purge_during_write_n(2, FALSE);
+ test_end();
+}
+
+static void test_mail_cache_write_lost_during_purge2(void)
+{
+ test_begin("mail cache write lost during purge (2)");
+ test_mail_cache_purge_during_write_n(3, FALSE);
+ test_end();
+}
+
+static void test_mail_cache_write_autocommit(void)
+{
+ test_begin("mail cache write autocommit");
+ test_mail_cache_purge_during_write_n(2, TRUE);
+ test_end();
+}
+
+static void test_mail_cache_write_autocommit2(void)
+{
+ test_begin("mail cache write autocommit");
+ test_mail_cache_purge_during_write_n(3, TRUE);
+ test_end();
+}
+
+static size_t max_field_size(size_t max_size, size_t current_size)
+{
+ return max_size - current_size
+ - sizeof(struct mail_cache_record)
+ - sizeof(uint32_t) /* field_idx */
+ - sizeof(uint32_t); /* data_size */
+}
+
+static void test_mail_cache_delete_too_large_int(bool exceed_on_first_write)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .max_size = 1024,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct stat st;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo2");
+
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+
+ /* create cache file that is exactly max_size */
+ size_t field_size =
+ max_field_size(optimization_set.cache.max_size, st.st_size);
+ if (exceed_on_first_write) {
+ test_expect_error_string("Cache file too large");
+ field_size++;
+ }
+ char *field = i_malloc(field_size + 1);
+ memset(field, 'x', field_size);
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, field);
+ test_expect_no_more_errors();
+ i_free(field);
+
+ if (!exceed_on_first_write) {
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+ test_assert(st.st_size == 1024);
+
+ /* adding anything more will delete the cache. */
+ test_expect_error_string("Cache file too large");
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar1");
+ test_expect_no_more_errors();
+ }
+ test_assert(stat(ctx.index->cache->filepath, &st) < 0 && errno == ENOENT);
+
+ mail_index_view_close(&ctx.view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_delete_too_large(void)
+{
+ test_begin("mail cache delete too large");
+ test_mail_cache_delete_too_large_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_delete_too_large2(void)
+{
+ test_begin("mail cache delete too large (2)");
+ test_mail_cache_delete_too_large_int(TRUE);
+ test_end();
+}
+
+static void test_mail_cache_purge_too_large_int(bool exceed_size)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .max_size = 1024,
+ },
+ };
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct test_mail_cache_ctx ctx;
+ struct stat st;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ /* add two mails with some cache field and expunge the first mail */
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "bar2");
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ mail_index_expunge(trans, 1);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_mail_cache_index_sync(&ctx);
+
+ /* Add a second mail whose cache field size is exactly the
+ max_size [+1 if exceed_size] */
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+ size_t field_size = (exceed_size ? 1 : 0) +
+ max_field_size(optimization_set.cache.max_size, st.st_size);
+ char *field = i_malloc(field_size + 1);
+ memset(field, 'x', field_size);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, field);
+
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+ if (exceed_size)
+ test_assert((uoff_t)st.st_size < optimization_set.cache.max_size);
+ else
+ test_assert((uoff_t)st.st_size == optimization_set.cache.max_size);
+
+ /* make sure we still find the cache fields */
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "bar2"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, field));
+ mail_cache_view_close(&cache_view);
+
+ i_free(field);
+ if (exceed_size)
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ else
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ mail_index_view_close(&ctx.view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_purge_too_large(void)
+{
+ test_begin("mail cache purge too large");
+ test_mail_cache_purge_too_large_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_purge_too_large2(void)
+{
+ test_begin("mail cache purge too large (2)");
+ test_mail_cache_purge_too_large_int(TRUE);
+ test_end();
+}
+
+static void test_mail_cache_unexpectedly_lost_int(bool read_first)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ test_mail_cache_purge();
+
+ /* Unexpectedly delete the cache file under us */
+ i_unlink(ctx.cache->filepath);
+
+ if (read_first) {
+ /* the cache file is already open, so initial reading should
+ work without errors */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ mail_cache_view_close(&cache_view);
+
+ /* if we refresh the index we get new reset_id, which requires
+ reopening the cache and that fails */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_expect_error_string("test.dovecot.index.cache: No such file or directory");
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_expect_no_more_errors();
+ mail_cache_view_close(&cache_view);
+ } else {
+ test_expect_error_string("test.dovecot.index.cache: No such file or directory");
+ }
+
+ /* writing after losing the cache should still work */
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar1");
+ test_expect_no_more_errors();
+
+ /* verify that the second cache field is found, but first is lost */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field2.idx, "bar1"));
+ mail_cache_view_close(&cache_view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_unexpectedly_lost(void)
+{
+ test_begin("mail cache unexpectedly lost");
+ test_mail_cache_unexpectedly_lost_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_unexpectedly_lost2(void)
+{
+ test_begin("mail cache unexpectedly lost (2)");
+ test_mail_cache_unexpectedly_lost_int(TRUE);
+ test_end();
+}
+
+static void test_mail_cache_resetid_mismatch_int(bool read_first)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ const char *temp_cache_path;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* make a copy of the first cache file */
+ temp_cache_path = t_strdup_printf("%s.test", ctx.cache->filepath);
+ test_assert(link(ctx.cache->filepath, temp_cache_path) == 0);
+
+ if (read_first) {
+ /* use a secondary index to purge the cache */
+ test_mail_cache_purge();
+
+ /* Replace the new cache file with an old one */
+ test_assert(rename(temp_cache_path, ctx.cache->filepath) == 0);
+
+ /* the cache file is already open, so initial reading should
+ work without errors */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ mail_cache_view_close(&cache_view);
+
+ /* if we refresh the index we get new reset_id, which requires
+ reopening the cache and that fails */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+
+ test_expect_error_string("reset_id mismatch even after locking");
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_expect_no_more_errors();
+ mail_cache_view_close(&cache_view);
+ } else {
+ /* purge cache to update reset_id in index */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+
+ /* Replace the new cache file with an old one */
+ test_assert(rename(temp_cache_path, ctx.cache->filepath) == 0);
+
+ test_expect_error_string("reset_id mismatch even after locking");
+ }
+
+ /* writing should automatically fix the reset_id mismatch */
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar1");
+ test_expect_no_more_errors();
+
+ /* verify that the second cache field is found, but first is lost */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field2.idx, "bar1"));
+ mail_cache_view_close(&cache_view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_resetid_mismatch(void)
+{
+ test_begin("mail cache resetid mismatch");
+ test_mail_cache_resetid_mismatch_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_resetid_mismatch2(void)
+{
+ test_begin("mail cache resetid mismatch (2)");
+ test_mail_cache_resetid_mismatch_int(TRUE);
+ test_end();
+}
+
+enum test_drop {
+ TEST_DROP_NOTHING,
+ TEST_DROP_YES_TO_TEMP_FIRST,
+ TEST_DROP_YES_TO_TEMP_LAST,
+ TEST_DROP_TEMP_TO_NO,
+};
+
+static void test_mail_cache_purge_field_changes_int(enum test_drop drop)
+{
+ enum {
+ TEST_FIELD_NO,
+ TEST_FIELD_NO_FORCED,
+ TEST_FIELD_TEMP,
+ TEST_FIELD_TEMP_FORCED,
+ TEST_FIELD_YES,
+ TEST_FIELD_YES_FORCED,
+ };
+ struct mail_cache_field cache_fields[] = {
+ {
+ .name = "no",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO,
+ },
+ {
+ .name = "no-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED,
+ },
+ {
+ .name = "temp",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_TEMP,
+ },
+ {
+ .name = "temp-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_TEMP | MAIL_CACHE_DECISION_FORCED,
+ },
+ {
+ .name = "yes",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "yes-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES | MAIL_CACHE_DECISION_FORCED,
+ },
+ };
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .unaccessed_field_drop_secs = 61,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ struct mail_index_transaction *trans;
+ unsigned int i;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ /* add two mails with all of the cache fields */
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+
+ /* Create the cache file before registering any of the cache_fields
+ that we're testing. Otherwise our caching decisions are messed up
+ by purging (which is called to auto-create the cache). */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ mail_cache_register_fields(ctx.cache, cache_fields,
+ N_ELEMENTS(cache_fields));
+
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ for (i = 0; i < N_ELEMENTS(cache_fields); i++) {
+ const char *value = t_strdup_printf("%s-value",
+ cache_fields[i].name);
+ if ((cache_fields[i].decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) !=
+ MAIL_CACHE_DECISION_NO) {
+ mail_cache_add(cache_trans, 1, cache_fields[i].idx,
+ value, strlen(value));
+ mail_cache_add(cache_trans, 2, cache_fields[i].idx,
+ value, strlen(value));
+ }
+ }
+
+ /* day_stamp in index is used for deciding when a cache field needs to
+ be dropped. */
+ uint32_t day_stamp = 123456789;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, day_stamp),
+ &day_stamp, sizeof(day_stamp), FALSE);
+ /* day_first_uid[7] is used to determine which mails are "old" and
+ which mails are "new". [7] is the first "new" mail. */
+ uint32_t first_new_uid = 2;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, day_first_uid[7]),
+ &first_new_uid, sizeof(first_new_uid), FALSE);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ /* set the last_used time just at the boundary of being dropped or
+ being kept */
+ for (i = 0; i < ctx.cache->fields_count; i++) {
+ unsigned int secs = optimization_set.cache.unaccessed_field_drop_secs;
+ switch (drop) {
+ case TEST_DROP_NOTHING:
+ break;
+ case TEST_DROP_YES_TO_TEMP_FIRST:
+ secs++;
+ break;
+ case TEST_DROP_YES_TO_TEMP_LAST:
+ secs *= 2;
+ break;
+ case TEST_DROP_TEMP_TO_NO:
+ secs *= 2;
+ secs++;
+ break;
+ }
+ ctx.cache->fields[i].field.last_used = day_stamp - secs;
+ }
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ test_mail_cache_view_sync(&ctx);
+
+ /* verify that caching decisions are as expected after purging */
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO].idx].field.decision ==
+ MAIL_CACHE_DECISION_NO);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO_FORCED].idx].field.decision ==
+ (MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED));
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP_FORCED].idx].field.decision ==
+ (MAIL_CACHE_DECISION_TEMP | MAIL_CACHE_DECISION_FORCED));
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES_FORCED].idx].field.decision ==
+ (MAIL_CACHE_DECISION_YES | MAIL_CACHE_DECISION_FORCED));
+
+ switch (drop) {
+ case TEST_DROP_NOTHING:
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP].idx].field.decision ==
+ MAIL_CACHE_DECISION_TEMP);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES].idx].field.decision ==
+ MAIL_CACHE_DECISION_YES);
+ break;
+ case TEST_DROP_YES_TO_TEMP_FIRST:
+ case TEST_DROP_YES_TO_TEMP_LAST:
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP].idx].field.decision ==
+ MAIL_CACHE_DECISION_TEMP);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES].idx].field.decision ==
+ MAIL_CACHE_DECISION_TEMP);
+ break;
+ case TEST_DROP_TEMP_TO_NO:
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP].idx].field.decision ==
+ MAIL_CACHE_DECISION_NO);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES].idx].field.decision ==
+ MAIL_CACHE_DECISION_NO);
+ break;
+ }
+
+ /* verify that cache fields exist as expected after purging */
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_NO].idx, NULL));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_NO].idx, NULL));
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_NO_FORCED].idx, NULL));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_NO_FORCED].idx, NULL));
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_TEMP].idx, NULL));
+ if (drop == TEST_DROP_TEMP_TO_NO)
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_TEMP].idx, NULL));
+ else
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_TEMP].idx, "temp-value"));
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_TEMP_FORCED].idx, NULL));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_TEMP_FORCED].idx, "temp-forced-value"));
+ if (drop != TEST_DROP_NOTHING)
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_YES].idx, NULL));
+ else
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_YES].idx, "yes-value"));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_YES_FORCED].idx, "yes-forced-value"));
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ mail_cache_view_close(&cache_view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_purge_field_changes(void)
+{
+ test_begin("mail cache purge field changes (nothing)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_NOTHING);
+ test_end();
+}
+
+static void test_mail_cache_purge_field_changes2(void)
+{
+ test_begin("mail cache purge field changes (yes -> temp, first)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_YES_TO_TEMP_FIRST);
+ test_end();
+}
+
+static void test_mail_cache_purge_field_changes3(void)
+{
+ test_begin("mail cache purge field changes (yes -> temp, last)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_YES_TO_TEMP_LAST);
+ test_end();
+}
+
+static void test_mail_cache_purge_field_changes4(void)
+{
+ test_begin("mail cache purge field changes (temp -> no)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_TEMP_TO_NO);
+ test_end();
+}
+
+static void test_mail_cache_purge_already_done(void)
+{
+ struct test_mail_cache_ctx ctx;
+
+ test_begin("mail cache purge already done");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ test_mail_cache_purge();
+ test_assert(mail_cache_purge(ctx.cache, 1, "test") == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+
+ test_assert(mail_cache_purge(ctx.cache, 2, "test") == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+
+ test_assert(mail_cache_purge(ctx.cache, 2, "test") == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_purge_bitmask(void)
+{
+ struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .unaccessed_field_drop_secs = 60,
+ },
+ };
+ struct mail_cache_field bitmask_field = {
+ .name = "bitmask",
+ .type = MAIL_CACHE_FIELD_BITMASK,
+ .field_size = 1,
+ .decision = MAIL_CACHE_DECISION_TEMP,
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+
+ test_begin("mail cache purge bitmask");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+ ioloop_time = 1000000;
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ mail_cache_register_fields(ctx.cache, &bitmask_field, 1);
+
+ test_mail_cache_update_day_first_uid7(&ctx, 3);
+
+ test_mail_cache_add_field(&ctx, 1, bitmask_field.idx, "\x01");
+ test_mail_cache_add_field(&ctx, 1, bitmask_field.idx, "\x02");
+ test_mail_cache_add_field(&ctx, 1, bitmask_field.idx, "\x04");
+ test_mail_cache_add_field(&ctx, 2, bitmask_field.idx, "\x01");
+ test_mail_cache_add_field(&ctx, 2, bitmask_field.idx, "\x02");
+ test_mail_cache_add_field(&ctx, 2, bitmask_field.idx, "\x04");
+
+ /* avoid dropping the field */
+ ctx.cache->fields[bitmask_field.idx].field.last_used = ioloop_time;
+
+ /* purge with TEMP decision, which causes the bitmask to be dropped */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, bitmask_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 2, bitmask_field.idx, NULL));
+ mail_cache_view_close(&cache_view);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+
+static void
+test_mail_cache_update_need_purge_continued_records_int(bool big_min_size)
+{
+ struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .purge_min_size = big_min_size ? 1024*1024 : 1,
+ .purge_continued_percentage = 30,
+ },
+ };
+ char value[30];
+ struct test_mail_cache_ctx ctx;
+ uint32_t seq;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ for (seq = 1; seq <= 100; seq++) {
+ i_snprintf(value, sizeof(value), "foo%d", seq);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, value);
+ }
+
+ /* up to 29% no need to purge */
+ for (seq = 1; seq <= 29; seq++) {
+ i_snprintf(value, sizeof(value), "bar%d", seq);
+ test_mail_cache_add_field(&ctx, seq, ctx.cache_field2.idx, value);
+ }
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+
+ /* at 30% need to purge */
+ test_mail_cache_add_field(&ctx, 30, ctx.cache_field2.idx, "bar30");
+ if (big_min_size)
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ else
+ test_assert(ctx.cache->need_purge_file_seq == ctx.cache->hdr->file_seq);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_update_need_purge_continued_records(void)
+{
+ test_begin("mail cache update need purge continued records");
+ test_mail_cache_update_need_purge_continued_records_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_update_need_purge_continued_records2(void)
+{
+ test_begin("mail cache update need purge continued records (2)");
+ test_mail_cache_update_need_purge_continued_records_int(TRUE);
+ test_end();
+}
+
+static void
+test_mail_cache_update_need_purge_deleted_records_int(bool big_min_size)
+{
+ struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .purge_min_size = big_min_size ? 1024*1024 : 1,
+ .purge_delete_percentage = 30,
+ },
+ };
+ char value[30];
+ struct mail_index_transaction *trans;
+ struct test_mail_cache_ctx ctx;
+ uint32_t seq;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ for (seq = 1; seq <= 100; seq++) {
+ i_snprintf(value, sizeof(value), "foo%d", seq);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, value);
+ }
+
+ /* up to 29% no need to purge */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ for (seq = 1; seq <= 29; seq++) {
+ i_snprintf(value, sizeof(value), "bar%d", seq);
+ mail_index_expunge(trans, seq);
+ }
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_mail_cache_index_sync(&ctx);
+
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ test_assert(mail_cache_reopen(ctx.cache) == 1);
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+
+ /* at 30% need to purge */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ mail_index_expunge(trans, 1);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ /* syncing will internally purge if !big_min_size */
+ test_mail_cache_index_sync(&ctx);
+
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ test_assert(mail_cache_reopen(ctx.cache) == 1);
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ if (big_min_size)
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ else
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_update_need_purge_deleted_records(void)
+{
+ test_begin("mail cache update need purge deleted records");
+ test_mail_cache_update_need_purge_deleted_records_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_update_need_purge_deleted_records2(void)
+{
+ test_begin("mail cache update need purge deleted records (2)");
+ test_mail_cache_update_need_purge_deleted_records_int(TRUE);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_cache_read_during_purge,
+ test_mail_cache_write_during_purge,
+ test_mail_cache_purge_while_cache_locked,
+ test_mail_cache_write_lost_during_purge,
+ test_mail_cache_write_lost_during_purge2,
+ test_mail_cache_write_autocommit,
+ test_mail_cache_write_autocommit2,
+ test_mail_cache_delete_too_large,
+ test_mail_cache_delete_too_large2,
+ test_mail_cache_purge_too_large,
+ test_mail_cache_purge_too_large2,
+ test_mail_cache_unexpectedly_lost,
+ test_mail_cache_unexpectedly_lost2,
+ test_mail_cache_resetid_mismatch,
+ test_mail_cache_resetid_mismatch2,
+ test_mail_cache_purge_field_changes,
+ test_mail_cache_purge_field_changes2,
+ test_mail_cache_purge_field_changes3,
+ test_mail_cache_purge_field_changes4,
+ test_mail_cache_purge_already_done,
+ test_mail_cache_purge_bitmask,
+ test_mail_cache_update_need_purge_continued_records,
+ test_mail_cache_update_need_purge_continued_records2,
+ test_mail_cache_update_need_purge_deleted_records,
+ test_mail_cache_update_need_purge_deleted_records2,
+ NULL
+ };
+ return test_run(test_functions);
+}