This change adds a new cleanup mode that avoids cleanup having re-traverse the directories the index pass just looked at. Additionally, we efficiently query the Xapian database by walking the term list instead of doing multiple point-wise path lookups. I'd noticed that most of my time in mu's cleanup pass consisted of B-tree lookups in Xapian (one 8KB pread64 at a time). The point lookups forced Xapian to traverse from the root of the B-tree to the leaf for every single message. Additionally, in order to join on the message path, we had to do *another* B-tree traversal after locating each message term. Now we just walk the terms in order, which is much more efficient, as we touch each B-tree node only once. On my system, with 1371861 total messages, the total time of mu index (no lazy check): --nocleanup: 3.6s incremental cleanup: 4.2s (0.6s in cleanup) legacy cleanup: 5.2s (1.6s in cleanup) With the new mode, we save 1.0s of the 1.6s cleanup, so we're ~63% faster. But the incremental cleanup works even better with lazy checking. If I enable --lazy-check, dirty only my INBOX (360778 messages), and run index, I get: --nocleanup: 0.9s incremental cleanup: 1.1s (0.2s in cleanup) legacy cleanup: 2.5s (1.6s in cleanup) We save 1.4s out of 1.6s for ~88% speedup. This change also fixes a timestamp bug: we should be storing the *start* time of the index pass in metadata, not the end time, so that on the next index pass, we notice messages that arrived between the two times. All tests pass. You can set the environment variable MU_NO_INCREMENTAL_CLEANUP to use the legacy cleanup path instead.
788 lines
19 KiB
C++
788 lines
19 KiB
C++
/*
|
|
** Copyright (C) 2020-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify it
|
|
** under the terms of the GNU General Public License as published by the
|
|
** Free Software Foundation; either version 3, or (at your option) any
|
|
** later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software Foundation,
|
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**
|
|
*/
|
|
|
|
#include "mu-indexer.hh"
|
|
|
|
#include <config.h>
|
|
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <vector>
|
|
#include <thread>
|
|
#include <atomic>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <chrono>
|
|
#include <string_view>
|
|
#include <ranges>
|
|
using namespace std::chrono_literals;
|
|
|
|
#include "mu-store.hh"
|
|
|
|
#include "mu-scanner.hh"
|
|
#include "utils/mu-async-queue.hh"
|
|
#include "utils/mu-error.hh"
|
|
|
|
#include "utils/mu-utils-file.hh"
|
|
|
|
using namespace Mu;
|
|
|
|
struct IndexState {
|
|
enum State { Idle,
|
|
Scanning,
|
|
Finishing,
|
|
Cleaning,
|
|
Aborting,
|
|
};
|
|
static const char* name(State s) {
|
|
switch (s) {
|
|
case Idle:
|
|
return "idle";
|
|
case Scanning:
|
|
return "scanning";
|
|
case Finishing:
|
|
return "finishing";
|
|
case Cleaning:
|
|
return "cleaning";
|
|
case Aborting:
|
|
return "aborting";
|
|
default:
|
|
return "<error>";
|
|
}
|
|
}
|
|
|
|
bool operator==(State rhs) const {
|
|
return state_.load() == rhs;
|
|
}
|
|
bool operator!=(State rhs) const {
|
|
return state_.load() != rhs;
|
|
}
|
|
void change_to(State new_state) {
|
|
mu_debug("changing indexer state {}->{}", name((State)state_),
|
|
name((State)new_state));
|
|
state_.store(new_state);
|
|
}
|
|
|
|
private:
|
|
std::atomic<State> state_{Idle};
|
|
};
|
|
|
|
struct Indexer::Private {
|
|
Private(Mu::Store& store)
|
|
: store_{store}, scanner_{store_.root_maildir(),
|
|
[this](auto&& path,
|
|
auto&& statbuf, auto&& info) {
|
|
return handler(path, statbuf, info);
|
|
}},
|
|
max_message_size_{store_.config().get<Mu::Config::Id::MaxMessageSize>()},
|
|
was_empty_{store.empty()} {
|
|
|
|
mu_message("created indexer for {} -> "
|
|
"{} (batch-size: {}; was-empty: {}; ngrams: {})",
|
|
store.root_maildir(), store.path(),
|
|
store.config().get<Mu::Config::Id::BatchSize>(),
|
|
was_empty_,
|
|
store.config().get<Mu::Config::Id::SupportNgrams>());
|
|
}
|
|
|
|
~Private() {
|
|
stop();
|
|
}
|
|
|
|
bool dir_predicate(const std::string& path, const struct dirent* dirent) const;
|
|
bool handler(const std::string& fullpath, struct stat* statbuf, Scanner::HandleType htype);
|
|
|
|
void maybe_start_worker();
|
|
|
|
void scan_worker();
|
|
|
|
bool add_message(const std::string& path);
|
|
|
|
void cleanup_from_scratch();
|
|
void cleanup_incremental();
|
|
bool start(const Indexer::Config& conf, bool block);
|
|
bool stop();
|
|
|
|
bool is_running() const { return state_ != IndexState::Idle; }
|
|
|
|
Indexer::Config conf_;
|
|
Store& store_;
|
|
Scanner scanner_;
|
|
const size_t max_message_size_;
|
|
|
|
::time_t dirstamp_{};
|
|
std::thread scanner_worker_;
|
|
|
|
struct WorkItem {
|
|
std::string full_path;
|
|
enum Type {
|
|
Dir,
|
|
File
|
|
};
|
|
Type type;
|
|
};
|
|
|
|
void handle_item(WorkItem&& item);
|
|
|
|
Progress progress_{};
|
|
IndexState state_{};
|
|
std::mutex lock_, w_lock_;
|
|
Option<time_t> started_;
|
|
std::atomic<time_t> completed_{};
|
|
bool was_empty_{};
|
|
|
|
uint64_t last_index_{};
|
|
|
|
// pathnames we've seen traversing the maildir hierarchy. entries
|
|
// with a trailing slash are maildir directories we've skipped as
|
|
// up-to-date.
|
|
std::vector<std::string> seen_maildir_paths_;
|
|
};
|
|
|
|
bool
|
|
Indexer::Private::handler(const std::string& fullpath, struct stat* statbuf,
|
|
Scanner::HandleType htype)
|
|
{
|
|
switch (htype) {
|
|
case Scanner::HandleType::EnterDir:
|
|
case Scanner::HandleType::EnterNewCur: {
|
|
if (fullpath.length() > MaxTermLength) {
|
|
// currently the db uses the path as a key, and
|
|
// therefore it cannot be too long. We'd get an error
|
|
// later anyway but for now it's useful for surviving
|
|
// circular symlinks
|
|
mu_warning("'{}' is too long; ignore", fullpath);
|
|
return false;
|
|
}
|
|
|
|
// in lazy-mode, we ignore this dir if its dirstamp suggest it
|
|
// is up-to-date (this is _not_ always true; hence we call it
|
|
// lazy-mode); only for actual message dirs, since the dir
|
|
// tstamps may not bubble up.
|
|
dirstamp_ = store_.dirstamp(fullpath);
|
|
if (conf_.lazy_check && dirstamp_ >= statbuf->st_ctime &&
|
|
htype == Scanner::HandleType::EnterNewCur) {
|
|
mu_debug("skip {} (seems up-to-date: {:%FT%T} >= {:%FT%T})",
|
|
fullpath, mu_time(dirstamp_), mu_time(statbuf->st_ctime));
|
|
seen_maildir_paths_.emplace_back(fullpath + "/");
|
|
return false;
|
|
}
|
|
|
|
// don't index dirs with '.noindex'
|
|
auto noindex = ::access((fullpath + "/.noindex").c_str(), F_OK) == 0;
|
|
if (noindex) {
|
|
mu_debug("skip {} (has .noindex)", fullpath);
|
|
return false; // don't descend into this dir.
|
|
}
|
|
|
|
// don't index dirs with '.noupdate', unless we do a full
|
|
// (re)index.
|
|
if (!conf_.ignore_noupdate) {
|
|
auto noupdate = ::access((fullpath + "/.noupdate").c_str(), F_OK) == 0;
|
|
if (noupdate) {
|
|
mu_debug("skip {} (has .noupdate)", fullpath);
|
|
seen_maildir_paths_.emplace_back(fullpath + "/");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
mu_debug("checked {}", fullpath);
|
|
return true;
|
|
}
|
|
case Scanner::HandleType::LeaveDir: {
|
|
handle_item({fullpath, WorkItem::Type::Dir});
|
|
return true;
|
|
}
|
|
|
|
case Scanner::HandleType::File: {
|
|
++progress_.checked;
|
|
seen_maildir_paths_.push_back(fullpath);
|
|
|
|
if (conf_.lazy_check && static_cast<uint64_t>(statbuf->st_ctime) < last_index_) {
|
|
// in lazy mode, ignore the file if it has not changed
|
|
// since the last indexing op.
|
|
return false;
|
|
}
|
|
|
|
if (static_cast<size_t>(statbuf->st_size) > max_message_size_) {
|
|
mu_debug("skip {} (too big: {} bytes)", fullpath, statbuf->st_size);
|
|
return false;
|
|
}
|
|
// if the message is not in the db yet, or not up-to-date, queue
|
|
// it for updating/inserting.
|
|
if (statbuf->st_ctime <= dirstamp_ && store_.contains_message(fullpath))
|
|
return false;
|
|
|
|
handle_item({fullpath, WorkItem::Type::File});
|
|
return true;
|
|
}
|
|
default:
|
|
g_return_val_if_reached(false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool
|
|
Indexer::Private::add_message(const std::string& path)
|
|
{
|
|
/*
|
|
* Having the lock here makes things a _lot_ slower.
|
|
*
|
|
* The reason for having the lock is some helgrind warnings;
|
|
* but it believed those are _false alarms_
|
|
* https://gitlab.gnome.org/GNOME/glib/-/issues/2662
|
|
*/
|
|
//std::unique_lock lock{w_lock_};
|
|
auto msg{Message::make_from_path(path, store_.message_options())};
|
|
if (!msg) {
|
|
mu_warning("failed to create message from {}: {}", path, msg.error().what());
|
|
return false;
|
|
}
|
|
// if the store was empty, we know that the message is completely new
|
|
// and can use the fast path (Xapians 'add_document' rather than
|
|
// 'replace_document)
|
|
auto res = store_.consume_message(std::move(msg.value()), was_empty_);
|
|
if (!res) {
|
|
mu_warning("failed to add message @ {}: {}", path, res.error().what());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void
|
|
Indexer::Private::handle_item(WorkItem&& item)
|
|
{
|
|
try {
|
|
switch (item.type) {
|
|
case WorkItem::Type::File: {
|
|
if (G_LIKELY(add_message(item.full_path)))
|
|
++progress_.updated;
|
|
} break;
|
|
case WorkItem::Type::Dir:
|
|
store_.set_dirstamp(item.full_path, started_.value());
|
|
break;
|
|
default:
|
|
g_warn_if_reached();
|
|
break;
|
|
}
|
|
} catch (const Mu::Error& er) {
|
|
mu_warning("error adding message @ {}: {}", item.full_path, er.what());
|
|
}
|
|
}
|
|
|
|
void
|
|
Indexer::Private::cleanup_from_scratch()
|
|
{
|
|
mu_debug("starting cleanup without using scan results");
|
|
|
|
std::vector<Store::Id> orphans; // store messages without files.
|
|
|
|
using DirFiles = std::unordered_set<std::string>;
|
|
std::unordered_map<std::string, DirFiles> dir_cache;
|
|
|
|
// get a set of file names in this directory.
|
|
const auto get_dir_files = [](const std::string& path) -> DirFiles {
|
|
DirFiles ret;
|
|
if (auto dir{::opendir(path.c_str())}; dir) {
|
|
dirent* dentry{};
|
|
while ((dentry = ::readdir(dir))) {
|
|
ret.emplace(dentry->d_name);
|
|
}
|
|
::closedir(dir);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
// is the file present in our set?
|
|
const auto is_file_present = [&](const std::string& path) -> bool {
|
|
std::string dir = dirname(path);
|
|
auto [it, inserted] = dir_cache.try_emplace(dir);
|
|
DirFiles& dir_files = it->second;
|
|
if (inserted) {
|
|
dir_files = get_dir_files(dir);
|
|
}
|
|
return dir_files.find(basename(path)) != dir_files.end();
|
|
};
|
|
|
|
store_.for_each_message_path([&](Store::Id id, const std::string& path) {
|
|
if (!is_file_present(path)) {
|
|
mu_debug("cannot read {} (id={}); queuing for removal from store",
|
|
path, id);
|
|
orphans.emplace_back(id);
|
|
}
|
|
|
|
return state_ == IndexState::Cleaning;
|
|
});
|
|
|
|
if (orphans.empty())
|
|
mu_debug("nothing to clean up");
|
|
else {
|
|
mu_debug("removing {} stale message(s) from store", orphans.size());
|
|
store_.remove_messages(orphans);
|
|
progress_.removed += orphans.size();
|
|
}
|
|
}
|
|
|
|
void
|
|
Indexer::Private::cleanup_incremental()
|
|
{
|
|
mu_debug("starting cleanup after scan");
|
|
|
|
// Sort the seen paths into the same order Xapian will give its terms to us
|
|
std::vector<std::string> fs_terms;
|
|
fs_terms.reserve(seen_maildir_paths_.size());
|
|
for (std::string_view fullpath : seen_maildir_paths_)
|
|
fs_terms.emplace_back(field_from_id(Field::Id::Path).xapian_term(fullpath));
|
|
std::ranges::sort(fs_terms);
|
|
|
|
// Discard duplicates from fs_terms in case two paths collided to one term, e.g. over
|
|
// case. That shouldn't happen, but be correct-ish if it does.
|
|
auto [new_end, old_end] = std::ranges::unique(fs_terms);
|
|
if (new_end != old_end) {
|
|
mu_warning("collisions under term normalization: using regular cleanup");
|
|
cleanup_from_scratch();
|
|
return;
|
|
}
|
|
|
|
fs_terms.erase(new_end, old_end);
|
|
seen_maildir_paths_.clear();
|
|
|
|
// Walk through all the path terms. If the DB has a path term that we didn't see in our
|
|
// filesystem walk above, add it the orphans list for removal from the DB. If we were in
|
|
// lazy scan mode, we may have skipped some directories entirely: these are represented by
|
|
// entries in fs_terms with a trailing slash. When we see one, we deem all DB entries
|
|
// that have the skip entry as a prefix as present.
|
|
|
|
size_t fs_terms_pos = 0;
|
|
auto current_fs_term = [&]() -> std::string_view {
|
|
if (fs_terms_pos < fs_terms.size())
|
|
return fs_terms[fs_terms_pos];
|
|
// N.B. '~' compares greater than the start of any field shortcut, so use it as an
|
|
// after-the-end sentinel.
|
|
return "~";
|
|
};
|
|
|
|
std::vector<std::string> orphan_terms;
|
|
|
|
auto handle_db_term = [&](std::string_view db_term) {
|
|
for (;;) {
|
|
std::string_view fs_term = current_fs_term();
|
|
bool is_wildcard = fs_term.ends_with('/');
|
|
if (is_wildcard && db_term.starts_with(fs_term)) {
|
|
return true;
|
|
}
|
|
int cmp = db_term.compare(fs_term);
|
|
if (cmp < 0) {
|
|
mu_debug("orphan in db={} but not fs={}", db_term, fs_term);
|
|
orphan_terms.emplace_back(db_term);
|
|
return true;
|
|
}
|
|
|
|
if (cmp == 0) {
|
|
++fs_terms_pos;
|
|
return true;
|
|
}
|
|
|
|
++fs_terms_pos;
|
|
// FS has an entry not in the DB. If not a directory, we should have
|
|
// indexed it.
|
|
if (!is_wildcard)
|
|
mu_warning("unexpectedly unindexed message: {}", fs_term);
|
|
}
|
|
};
|
|
|
|
store_.for_each_term(Field::Id::Path, handle_db_term);
|
|
|
|
if (orphan_terms.empty())
|
|
mu_debug("nothing to clean up");
|
|
else {
|
|
size_t nr_removed = store_.remove_messages_by_term(
|
|
orphan_terms, [&](size_t nr_cleaned_up_so_far) {
|
|
progress_.removed = nr_cleaned_up_so_far;
|
|
});
|
|
mu_debug("removing {} stale message(s) from store", nr_removed);
|
|
progress_.removed = nr_removed;
|
|
if (nr_removed != orphan_terms.size())
|
|
mu_warning("should have removed {} messages but actually removed {}",
|
|
orphan_terms.size(), nr_removed);
|
|
}
|
|
}
|
|
|
|
void
|
|
Indexer::Private::scan_worker()
|
|
{
|
|
progress_.reset();
|
|
seen_maildir_paths_.clear();
|
|
started_ = time(NULL);
|
|
|
|
if (conf_.scan) {
|
|
mu_debug("starting scanner");
|
|
if (!scanner_.start()) { // blocks.
|
|
mu_warning("failed to start scanner");
|
|
state_.change_to(IndexState::Idle);
|
|
return;
|
|
}
|
|
mu_debug("scanner finished");
|
|
}
|
|
|
|
enum class CleanupKind {
|
|
None,
|
|
FromScratch,
|
|
Incremental,
|
|
};
|
|
|
|
CleanupKind cleanup_kind = CleanupKind::Incremental;
|
|
bool aborted = state_ == IndexState::Aborting;
|
|
|
|
if (cleanup_kind >= CleanupKind::None && !conf_.cleanup) {
|
|
mu_debug("cleanup: not running as requested");
|
|
cleanup_kind = CleanupKind::None;
|
|
}
|
|
|
|
if (cleanup_kind > CleanupKind::None && aborted) {
|
|
mu_debug("cleanup: disabling because indexer aborted");
|
|
cleanup_kind = CleanupKind::None;
|
|
}
|
|
|
|
if (cleanup_kind >= CleanupKind::Incremental &&
|
|
g_getenv("MU_NO_INCREMENTAL_CLEANUP")) {
|
|
mu_debug("cleanup: not using incremental: MU_NO_INCREMENTAL_CLEANUP in environ");
|
|
cleanup_kind = CleanupKind::FromScratch;
|
|
}
|
|
|
|
if (cleanup_kind >= CleanupKind::Incremental && !conf_.scan) {
|
|
mu_debug("cleanup: not using incremental: scan not done");
|
|
cleanup_kind = CleanupKind::FromScratch;
|
|
}
|
|
|
|
state_.change_to(IndexState::Cleaning);
|
|
|
|
switch (cleanup_kind) {
|
|
case CleanupKind::None:
|
|
break;
|
|
case CleanupKind::FromScratch:
|
|
cleanup_from_scratch();
|
|
break;
|
|
case CleanupKind::Incremental:
|
|
cleanup_incremental();
|
|
break;
|
|
}
|
|
|
|
aborted = state_ == IndexState::Aborting;
|
|
if (!aborted) {
|
|
// Store started time, not ending time, so that next time we run we know to scan
|
|
// anything that appeared during our scan.
|
|
store_.config().set<Mu::Config::Id::LastIndex>(started_.value());
|
|
}
|
|
|
|
completed_ = ::time({});
|
|
// attempt to commit to disk.
|
|
store_.xapian_db().request_commit(true);
|
|
state_.change_to(IndexState::Idle);
|
|
started_ = Nothing;
|
|
}
|
|
|
|
bool
|
|
Indexer::Private::start(const Indexer::Config& conf, bool block)
|
|
{
|
|
stop();
|
|
|
|
conf_ = conf;
|
|
|
|
if (store_.empty() && conf_.lazy_check) {
|
|
mu_debug("turn off lazy check since we have an empty store");
|
|
conf_.lazy_check = false;
|
|
}
|
|
|
|
mu_debug("starting indexer");
|
|
mu_debug("indexing: {}; clean-up: {}", conf_.scan ? "yes" : "no",
|
|
conf_.cleanup ? "yes" : "no");
|
|
|
|
// remember the _previous_ indexing, so in lazy mode we can skip
|
|
// those files.
|
|
last_index_ = store_.config().get<Mu::Config::Id::LastIndex>();
|
|
|
|
state_.change_to(IndexState::Scanning);
|
|
/* kick the disk-scanner thread */
|
|
scanner_worker_ = std::thread([this] { scan_worker(); });
|
|
|
|
mu_debug("started indexer in {}-mode", block ? "blocking" : "non-blocking");
|
|
if (block) {
|
|
while(is_running()) {
|
|
using namespace std::chrono_literals;
|
|
std::this_thread::sleep_for(100ms);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
Indexer::Private::stop()
|
|
{
|
|
if (state_ != IndexState::Idle)
|
|
state_.change_to(IndexState::Aborting);
|
|
scanner_.stop();
|
|
|
|
if (scanner_worker_.joinable())
|
|
scanner_worker_.join();
|
|
|
|
state_.change_to(IndexState::Idle);
|
|
|
|
return true;
|
|
}
|
|
|
|
Indexer::Indexer(Store& store)
|
|
: priv_{std::make_unique<Private>(store)}
|
|
{}
|
|
|
|
Indexer::~Indexer() = default;
|
|
|
|
bool
|
|
Indexer::start(const Indexer::Config& conf, bool block)
|
|
{
|
|
const auto mdir{priv_->store_.root_maildir()};
|
|
if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0)) {
|
|
mu_critical("'{}' is not readable: {}", mdir, g_strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
std::lock_guard lock(priv_->lock_);
|
|
if (is_running())
|
|
return true;
|
|
|
|
return priv_->start(conf, block);
|
|
}
|
|
|
|
bool
|
|
Indexer::stop()
|
|
{
|
|
std::lock_guard lock{priv_->lock_};
|
|
|
|
if (!is_running())
|
|
return true;
|
|
|
|
mu_debug("stopping indexer");
|
|
return priv_->stop();
|
|
}
|
|
|
|
bool
|
|
Indexer::is_running() const
|
|
{
|
|
return priv_->is_running();
|
|
}
|
|
|
|
const Indexer::Progress&
|
|
Indexer::progress() const
|
|
{
|
|
priv_->progress_.running = priv_->state_ == IndexState::Idle ? false : true;
|
|
|
|
return priv_->progress_;
|
|
}
|
|
|
|
::time_t
|
|
Indexer::completed() const
|
|
{
|
|
return priv_->completed_;
|
|
}
|
|
|
|
|
|
#if BUILD_TESTS
|
|
#include "mu-test-utils.hh"
|
|
|
|
static void
|
|
test_index_basic()
|
|
{
|
|
allow_warnings();
|
|
|
|
TempDir tdir;
|
|
auto store = Store::make_new(tdir.path(), MU_TESTMAILDIR2);
|
|
assert_valid_result(store);
|
|
g_assert_true(store->empty());
|
|
|
|
Indexer& idx{store->indexer()};
|
|
|
|
g_assert_false(idx.is_running());
|
|
g_assert_true(idx.stop());
|
|
g_assert_cmpuint(idx.completed(),==, 0);
|
|
|
|
const auto& prog{idx.progress()};
|
|
g_assert_false(prog.running);
|
|
g_assert_cmpuint(prog.checked,==, 0);
|
|
g_assert_cmpuint(prog.updated,==, 0);
|
|
g_assert_cmpuint(prog.removed,==, 0);
|
|
|
|
mu_test_skip_valgrind_return();
|
|
|
|
Indexer::Config conf{};
|
|
conf.ignore_noupdate = true;
|
|
|
|
{
|
|
const auto start{time({})};
|
|
g_assert_true(idx.start(conf));
|
|
while (idx.is_running())
|
|
g_usleep(10000);
|
|
|
|
g_assert_false(idx.is_running());
|
|
g_assert_true(idx.stop());
|
|
|
|
g_assert_cmpuint(idx.completed() - start, <, 5);
|
|
|
|
g_assert_false(prog.running);
|
|
g_assert_cmpuint(prog.checked,==, 14);
|
|
g_assert_cmpuint(prog.updated,==, 14);
|
|
g_assert_cmpuint(prog.removed,==, 0);
|
|
|
|
g_assert_cmpuint(store->size(),==,14);
|
|
}
|
|
|
|
conf.lazy_check = true;
|
|
conf.ignore_noupdate = false;
|
|
|
|
{
|
|
const auto start{time({})};
|
|
g_assert_true(idx.start(conf));
|
|
while (idx.is_running())
|
|
g_usleep(10000);
|
|
|
|
g_assert_false(idx.is_running());
|
|
g_assert_true(idx.stop());
|
|
|
|
g_assert_cmpuint(idx.completed() - start, <, 3);
|
|
|
|
g_assert_false(prog.running);
|
|
g_assert_cmpuint(prog.checked,==, 0);
|
|
g_assert_cmpuint(prog.updated,==, 0);
|
|
g_assert_cmpuint(prog.removed,==, 0);
|
|
|
|
g_assert_cmpuint(store->size(),==, 14);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
test_index_lazy()
|
|
{
|
|
allow_warnings();
|
|
|
|
mu_test_skip_valgrind_return();
|
|
|
|
TempDir tdir;
|
|
auto store = Store::make_new(tdir.path(), MU_TESTMAILDIR2);
|
|
assert_valid_result(store);
|
|
g_assert_true(store->empty());
|
|
Indexer& idx{store->indexer()};
|
|
|
|
Indexer::Config conf{};
|
|
conf.lazy_check = true;
|
|
conf.ignore_noupdate = false;
|
|
|
|
const auto start{time({})};
|
|
g_assert_true(idx.start(conf));
|
|
while (idx.is_running())
|
|
g_usleep(10000);
|
|
|
|
g_assert_false(idx.is_running());
|
|
g_assert_true(idx.stop());
|
|
|
|
g_assert_cmpuint(idx.completed() - start, <, 3);
|
|
|
|
const auto& prog{idx.progress()};
|
|
g_assert_false(prog.running);
|
|
g_assert_cmpuint(prog.checked,==, 6);
|
|
g_assert_cmpuint(prog.updated,==, 6);
|
|
g_assert_cmpuint(prog.removed,==, 0);
|
|
|
|
g_assert_cmpuint(store->size(),==, 6);
|
|
}
|
|
|
|
static void
|
|
test_index_cleanup()
|
|
{
|
|
allow_warnings();
|
|
|
|
TempDir tdir;
|
|
auto mdir = join_paths(tdir.path(), "Test");
|
|
{
|
|
auto res = run_command({"cp", "-r", MU_TESTMAILDIR2, mdir});
|
|
assert_valid_result(res);
|
|
g_assert_cmpuint(res->exit_code,==, 0);
|
|
}
|
|
|
|
auto store = Store::make_new(tdir.path(), mdir);
|
|
assert_valid_result(store);
|
|
g_assert_true(store->empty());
|
|
Indexer& idx{store->indexer()};
|
|
|
|
Indexer::Config conf{};
|
|
conf.ignore_noupdate = true;
|
|
|
|
g_assert_true(idx.start(conf));
|
|
while (idx.is_running())
|
|
g_usleep(10000);
|
|
|
|
g_assert_false(idx.is_running());
|
|
g_assert_true(idx.stop());
|
|
g_assert_cmpuint(store->size(),==, 14);
|
|
|
|
// remove a message
|
|
{
|
|
auto mpath = join_paths(mdir, "bar", "cur", "mail6");
|
|
auto res = run_command({"rm", mpath});
|
|
assert_valid_result(res);
|
|
g_assert_cmpuint(res->exit_code,==, 0);
|
|
}
|
|
|
|
// no cleanup, # stays the same
|
|
conf.cleanup = false;
|
|
g_assert_true(idx.start(conf));
|
|
while (idx.is_running())
|
|
g_usleep(10000);
|
|
g_assert_false(idx.is_running());
|
|
g_assert_true(idx.stop());
|
|
g_assert_cmpuint(store->size(),==, 14);
|
|
|
|
// cleanup, message is gone from store.
|
|
conf.cleanup = true;
|
|
g_assert_true(idx.start(conf));
|
|
while (idx.is_running())
|
|
g_usleep(10000);
|
|
g_assert_false(idx.is_running());
|
|
g_assert_true(idx.stop());
|
|
g_assert_cmpuint(store->size(),==, 13);
|
|
}
|
|
|
|
|
|
int
|
|
main(int argc, char* argv[])
|
|
{
|
|
mu_test_init(&argc, &argv);
|
|
|
|
g_test_add_func("/index/basic", test_index_basic);
|
|
g_test_add_func("/index/lazy", test_index_lazy);
|
|
g_test_add_func("/index/cleanup", test_index_cleanup);
|
|
|
|
return g_test_run();
|
|
|
|
}
|
|
#endif /*BUILD_TESTS*/
|