diff --git a/autotests/server/collectionschedulertest.cpp b/autotests/server/collectionschedulertest.cpp index 7c3fe2c7a..dca615b60 100644 --- a/autotests/server/collectionschedulertest.cpp +++ b/autotests/server/collectionschedulertest.cpp @@ -1,136 +1,136 @@ /* Copyright (c) 2019 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "fakeakonadiserver.h" #include "fakecollectionscheduler.h" #include using namespace Akonadi; using namespace Akonadi::Server; using TimePoint = CollectionScheduler::TimePoint; using namespace std::literals::chrono_literals; class CollectionSchedulerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { FakeAkonadiServer::instance()->init(); } void shouldInitializeSyncIntervals() { // WHEN FakeCollectionScheduler sched; sched.waitForInit(); const TimePoint now(std::chrono::steady_clock::now()); // THEN // Collections root (1), ColA (2), ColB (3), ColD (5), virtual (6) and virtual2 (7) // should have a check scheduled in 5 minutes (default value) for (qint64 collectionId : {1, 2, 3, 5, 6, 7}) { - QVERIFY(sched.nextScheduledTime(collectionId) > now + 4min); + QVERIFY2(sched.nextScheduledTime(collectionId) > now + 4min, qPrintable(QString::number(collectionId))); QVERIFY(sched.nextScheduledTime(collectionId) < now + 6min); } QCOMPARE(sched.nextScheduledTime(4).time_since_epoch(), TimePoint::duration::zero()); // ColC is skipped because syncPref=false QCOMPARE(sched.nextScheduledTime(314).time_since_epoch(), TimePoint::duration::zero()); // no such collection QVERIFY(sched.currentTimerInterval() > 4min); QVERIFY(sched.currentTimerInterval() < 6min); } // (not that this feature is really used right now, it defaults to 5 and CacheCleaner sets it to 5) void shouldObeyMinimumInterval() { // GIVEN FakeCollectionScheduler sched; // WHEN sched.setMinimumInterval(10); sched.waitForInit(); // THEN const TimePoint now(std::chrono::steady_clock::now()); QTRY_VERIFY(sched.nextScheduledTime(2).time_since_epoch() > TimePoint::duration::zero()); QVERIFY(sched.nextScheduledTime(2) > now + 9min); QVERIFY(sched.nextScheduledTime(2) < now + 11min); QVERIFY(sched.currentTimerInterval() > 9min); QVERIFY(sched.currentTimerInterval() < 11min); } void shouldRemoveAndAddCollectionFromSchedule() { // GIVEN FakeCollectionScheduler sched; sched.waitForInit(); const auto timeForRoot = sched.nextScheduledTime(1); const auto timeForColB = sched.nextScheduledTime(3); QVERIFY(sched.nextScheduledTime(2) <= timeForColB); // WHEN sched.collectionRemoved(2); // THEN QTRY_COMPARE(sched.nextScheduledTime(2).time_since_epoch(), TimePoint::duration::zero()); QCOMPARE(sched.nextScheduledTime(1), timeForRoot); // unchanged QCOMPARE(sched.nextScheduledTime(3), timeForColB); // unchanged QVERIFY(sched.currentTimerInterval() > 4min); // unchanged QVERIFY(sched.currentTimerInterval() < 6min); // unchanged // AND WHEN re-adding the collection QTest::qWait(1000); // we only have precision to the second... sched.collectionAdded(2); // THEN QTRY_VERIFY(sched.nextScheduledTime(2).time_since_epoch() > TimePoint::duration::zero()); // This is unchanged, even though it would normally have been 1s later. See "minor optimization" in scheduler. QCOMPARE(sched.nextScheduledTime(2), timeForColB); QCOMPARE(sched.nextScheduledTime(1), timeForRoot); // unchanged QCOMPARE(sched.nextScheduledTime(3), timeForColB); // unchanged QVERIFY(sched.currentTimerInterval() > 4min); // unchanged QVERIFY(sched.currentTimerInterval() < 6min); // unchanged } void shouldHonourIntervalChange() { // GIVEN FakeCollectionScheduler sched; sched.waitForInit(); const auto timeForColB = sched.nextScheduledTime(3); Collection colA = Collection::retrieveByName(QStringLiteral("Collection A")); QCOMPARE(colA.id(), 2); QVERIFY(sched.nextScheduledTime(2) <= timeForColB); // WHEN colA.setCachePolicyInherit(false); colA.setCachePolicyCheckInterval(20); // in minutes QVERIFY(colA.update()); sched.collectionChanged(2); // THEN // "in 20 minutes" is 15 minutes later than "in 5 minutes" QTRY_VERIFY(sched.nextScheduledTime(2) >= timeForColB + 14min); QVERIFY(sched.nextScheduledTime(2) <= timeForColB + 16min); QVERIFY(sched.currentTimerInterval() > 4min); // unchanged QVERIFY(sched.currentTimerInterval() < 6min); // unchanged } }; AKTEST_FAKESERVER_MAIN(CollectionSchedulerTest) #include "collectionschedulertest.moc" diff --git a/autotests/server/fakecollectionscheduler.cpp b/autotests/server/fakecollectionscheduler.cpp index 47d1f43bd..00a7a8d1d 100644 --- a/autotests/server/fakecollectionscheduler.cpp +++ b/autotests/server/fakecollectionscheduler.cpp @@ -1,55 +1,60 @@ /* Copyright (c) 2019 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "fakecollectionscheduler.h" using namespace Akonadi::Server; FakeCollectionScheduler::FakeCollectionScheduler(QObject *parent) : CollectionScheduler(QStringLiteral("FakeCollectionScheduler"), QThread::NormalPriority, parent) { } void FakeCollectionScheduler::waitForInit() { m_initCalled.acquire(); } void FakeCollectionScheduler::init() { CollectionScheduler::init(); m_initCalled.release(); } +bool FakeCollectionScheduler::shouldScheduleCollection(const Collection &collection) +{ + return (collection.syncPref() == Collection::True) || ((collection.syncPref() == Collection::Undefined) && collection.enabled()); +} + bool FakeCollectionScheduler::hasChanged(const Collection &collection, const Collection &changed) { Q_ASSERT(collection.id() == changed.id()); return collection.cachePolicyCheckInterval() != changed.cachePolicyCheckInterval(); } int FakeCollectionScheduler::collectionScheduleInterval(const Collection &collection) { return collection.cachePolicyCheckInterval(); } void FakeCollectionScheduler::collectionExpired(const Collection &collection) { Q_UNUSED(collection); // Nothing here. The granularity is in whole minutes, we don't have time to wait for that in a unittest. } diff --git a/autotests/server/fakecollectionscheduler.h b/autotests/server/fakecollectionscheduler.h index 92698d512..0af06c41f 100644 --- a/autotests/server/fakecollectionscheduler.h +++ b/autotests/server/fakecollectionscheduler.h @@ -1,52 +1,52 @@ /* Copyright (c) 2019 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef FAKECOLLECTIONSCHEDULER_H #define FAKECOLLECTIONSCHEDULER_H #include "collectionscheduler.h" #include namespace Akonadi { namespace Server { class FakeCollectionScheduler : public CollectionScheduler { Q_OBJECT public: FakeCollectionScheduler(QObject *parent = nullptr); void waitForInit(); protected: void init() override; - bool shouldScheduleCollection(const Collection &) override { return true; } + bool shouldScheduleCollection(const Collection &) override; bool hasChanged(const Collection &collection, const Collection &changed) override; int collectionScheduleInterval(const Collection &collection) override; void collectionExpired(const Collection &collection) override; private: QSemaphore m_initCalled; }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/collectionscheduler.cpp b/src/server/collectionscheduler.cpp index f3ffe1b89..c3989b1e7 100644 --- a/src/server/collectionscheduler.cpp +++ b/src/server/collectionscheduler.cpp @@ -1,346 +1,339 @@ /* Copyright (c) 2014 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionscheduler.h" #include "storage/datastore.h" #include "storage/selectquerybuilder.h" #include "akonadiserver_debug.h" #include #include #include using namespace std::literals::chrono_literals; namespace Akonadi { namespace Server { /** * @warning: QTimer's methods are not virtual, so it's necessary to always call * methods on pointer to PauseableTimer! */ class PauseableTimer : public QTimer { Q_OBJECT public: PauseableTimer(QObject *parent = nullptr) : QTimer(parent) { } void start(int interval) { mStarted = QDateTime::currentDateTimeUtc(); mPaused = QDateTime(); setInterval(interval); QTimer::start(interval); } void start() { start(interval()); } void stop() { mStarted = QDateTime(); mPaused = QDateTime(); QTimer::stop(); } Q_INVOKABLE void pause() { if (!isActive() || isPaused()) { return; } mPaused = QDateTime::currentDateTimeUtc(); QTimer::stop(); } Q_INVOKABLE void resume() { if (!isPaused()) { return; } const int remainder = interval() - (mStarted.secsTo(mPaused) * 1000); start(qMax(0, remainder)); mPaused = QDateTime(); // Update mStarted so that pause() can be called repeatedly mStarted = QDateTime::currentDateTimeUtc(); } bool isPaused() const { return mPaused.isValid(); } private: QDateTime mStarted; QDateTime mPaused; }; } // namespace Server } // namespace Akonadi using namespace Akonadi::Server; CollectionScheduler::CollectionScheduler(const QString &threadName, QThread::Priority priority, QObject *parent) : AkThread(threadName, priority, parent) { } CollectionScheduler::~CollectionScheduler() { } // Called in secondary thread void CollectionScheduler::quit() { delete mScheduler; mScheduler = nullptr; AkThread::quit(); } void CollectionScheduler::inhibit(bool inhibit) { if (inhibit) { const bool success = QMetaObject::invokeMethod(mScheduler, &PauseableTimer::pause, Qt::QueuedConnection); Q_ASSERT(success); Q_UNUSED(success); } else { const bool success = QMetaObject::invokeMethod(mScheduler, &PauseableTimer::resume, Qt::QueuedConnection); Q_ASSERT(success); Q_UNUSED(success); } } int CollectionScheduler::minimumInterval() const { return mMinInterval; } CollectionScheduler::TimePoint CollectionScheduler::nextScheduledTime(qint64 collectionId) const { QMutexLocker locker(&mScheduleLock); const auto i = constFind(collectionId); if (i != mSchedule.cend()) { return i.key(); } return {}; } std::chrono::milliseconds CollectionScheduler::currentTimerInterval() const { return std::chrono::milliseconds(mScheduler->isActive() ? mScheduler->interval() : 0); } void CollectionScheduler::setMinimumInterval(int intervalMinutes) { // No mutex -- you can only call this before starting the thread mMinInterval = intervalMinutes; } void CollectionScheduler::collectionAdded(qint64 collectionId) { Collection collection = Collection::retrieveById(collectionId); DataStore::self()->activeCachePolicy(collection); if (shouldScheduleCollection(collection)) { QMetaObject::invokeMethod(this, [this, collection]() {scheduleCollection(collection);}, Qt::QueuedConnection); } } void CollectionScheduler::collectionChanged(qint64 collectionId) { QMutexLocker locker(&mScheduleLock); const auto it = constFind(collectionId); if (it != mSchedule.cend()) { const Collection oldCollection = it.value(); Collection changed = Collection::retrieveById(collectionId); DataStore::self()->activeCachePolicy(changed); if (hasChanged(oldCollection, changed)) { if (shouldScheduleCollection(changed)) { locker.unlock(); // Scheduling the changed collection will automatically remove the old one QMetaObject::invokeMethod(this, [this, changed]() {scheduleCollection(changed);}, Qt::QueuedConnection); } else { locker.unlock(); // If the collection should no longer be scheduled then remove it collectionRemoved(collectionId); } } } else { // We don't know the collection yet, but maybe now it can be scheduled collectionAdded(collectionId); } } void CollectionScheduler::collectionRemoved(qint64 collectionId) { QMutexLocker locker(&mScheduleLock); auto it = find(collectionId); if (it != mSchedule.end()) { const bool reschedule = it == mSchedule.begin(); mSchedule.erase(it); // If we just remove currently scheduled collection, schedule the next one if (reschedule) { QMetaObject::invokeMethod(this, &CollectionScheduler::startScheduler, Qt::QueuedConnection); } } } // Called in secondary thread void CollectionScheduler::startScheduler() { QMutexLocker locker(&mScheduleLock); // Don't restart timer if we are paused. if (mScheduler->isPaused()) { return; } if (mSchedule.isEmpty()) { // Stop the timer. It will be started again once some collection is scheduled mScheduler->stop(); return; } // Get next collection to expire and start the timer const auto next = mSchedule.constBegin().key(); // TimePoint uses a signed representation internally (int64_t), so we get negative result when next is in the past const auto delayUntilNext = next - std::chrono::steady_clock::now(); mScheduler->start(qMax(0, std::chrono::duration_cast(delayUntilNext).count())); } // Called in secondary thread void CollectionScheduler::scheduleCollection(Collection collection, bool shouldStartScheduler) { QMutexLocker locker(&mScheduleLock); auto i = find(collection.id()); if (i != mSchedule.end()) { mSchedule.erase(i); } DataStore::self()->activeCachePolicy(collection); if (!shouldScheduleCollection(collection)) { return; } const int expireMinutes = qMax(mMinInterval, collectionScheduleInterval(collection)); TimePoint nextCheck(std::chrono::steady_clock::now() + std::chrono::minutes(expireMinutes)); // Check whether there's another check scheduled within a minute after this one. // If yes, then delay this check so that it's scheduled together with the others // This is a minor optimization to reduce wakeups and SQL queries auto it = constLowerBound(nextCheck); if (it != mSchedule.cend() && it.key() - nextCheck < 1min) { nextCheck = it.key(); // Also check whether there's another checked scheduled within a minute before // this one. } else if (it != mSchedule.cbegin()) { --it; if (nextCheck - it.key() < 1min) { nextCheck = it.key(); } } mSchedule.insert(nextCheck, collection); if (shouldStartScheduler && !mScheduler->isActive()) { locker.unlock(); startScheduler(); } } CollectionScheduler::ScheduleMap::const_iterator CollectionScheduler::constFind(qint64 collectionId) const { return std::find_if(mSchedule.cbegin(), mSchedule.cend(), [collectionId](const Collection &c) { return c.id() == collectionId; }); } CollectionScheduler::ScheduleMap::iterator CollectionScheduler::find(qint64 collectionId) { return std::find_if(mSchedule.begin(), mSchedule.end(), [collectionId](const Collection &c) { return c.id() == collectionId; }); } // separate method so we call the const version of QMap::lowerBound CollectionScheduler::ScheduleMap::const_iterator CollectionScheduler::constLowerBound(TimePoint timestamp) const { return mSchedule.lowerBound(timestamp); } // Called in secondary thread void CollectionScheduler::init() { AkThread::init(); mScheduler = new PauseableTimer(); mScheduler->setSingleShot(true); connect(mScheduler, &QTimer::timeout, this, &CollectionScheduler::schedulerTimeout); // Only retrieve enabled collections and referenced collections, we don't care // about anything else SelectQueryBuilder qb; - Query::Condition orCondition(Query::Or); - orCondition.addValueCondition(Collection::syncPrefFullColumnName(), Query::Equals, (int)Akonadi::Tristate::True); - Query::Condition andCondition(Query::And); - andCondition.addValueCondition(Collection::syncPrefFullColumnName(), Query::Equals, (int)Akonadi::Tristate::Undefined); - andCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true); - orCondition.addCondition(andCondition); - qb.addCondition(orCondition); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to query initial collections for scheduler!"; qCWarning(AKONADISERVER_LOG) << "Not a fatal error, no collections will be scheduled for sync or cache expiration!"; } const Collection::List collections = qb.result(); for (const Collection &collection : collections) { scheduleCollection(collection); } startScheduler(); } // Called in secondary thread void CollectionScheduler::schedulerTimeout() { QMutexLocker locker(&mScheduleLock); // Call stop() explicitly to reset the timer mScheduler->stop(); const auto timestamp = mSchedule.constBegin().key(); const QList collections = mSchedule.values(timestamp); mSchedule.remove(timestamp); locker.unlock(); for (const Collection &collection : collections) { collectionExpired(collection); scheduleCollection(collection, false); } startScheduler(); } #include "collectionscheduler.moc"