diff --git a/autotests/server/CMakeLists.txt b/autotests/server/CMakeLists.txt --- a/autotests/server/CMakeLists.txt +++ b/autotests/server/CMakeLists.txt @@ -26,6 +26,7 @@ set(common_SRCS unittestschema.cpp fakeconnection.cpp + fakecollectionscheduler.cpp fakedatastore.cpp fakeclient.cpp fakeakonadiserver.cpp @@ -90,6 +91,7 @@ add_server_test(parttypehelpertest.cpp) add_server_test(collectionstatisticstest.cpp) add_server_test(aggregatedfetchscopetest.cpp) +add_server_test(collectionschedulertest.cpp) if (SQLITE_FOUND) # tests using the fake server need the QSQLITE3 plugin add_server_test(partstreamertest.cpp) diff --git a/autotests/server/collectionschedulertest.cpp b/autotests/server/collectionschedulertest.cpp new file mode 100644 --- /dev/null +++ b/autotests/server/collectionschedulertest.cpp @@ -0,0 +1,122 @@ +/* + 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; + +class CollectionSchedulerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + FakeAkonadiServer::instance()->init(); + } + + void shouldInitializeSyncIntervals() + { + // WHEN + FakeCollectionScheduler sched; + sched.waitForInit(); + const qint64 now = QDateTime::currentSecsSinceEpoch(); + // 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 + 4 * 60); + QVERIFY(sched.nextScheduledTime(collectionId) < now + 6 * 60); + } + QCOMPARE(sched.nextScheduledTime(4), 0); // ColC is skipped because syncPref=false + QCOMPARE(sched.nextScheduledTime(314), 0); // no such collection + } + + // (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 qint64 now = QDateTime::currentSecsSinceEpoch(); + QTRY_VERIFY(sched.nextScheduledTime(2) > 0); + QVERIFY(sched.nextScheduledTime(2) > now + 9 * 60); + QVERIFY(sched.nextScheduledTime(2) < now + 11 * 60); + } + + 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), 0); + QCOMPARE(sched.nextScheduledTime(1), timeForRoot); // unchanged + QCOMPARE(sched.nextScheduledTime(3), timeForColB); // 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) > 0); + // 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 + } + + 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 + 14 * 60); + QVERIFY(sched.nextScheduledTime(2) <= timeForColB + 16 * 60); + } +}; + +AKTEST_FAKESERVER_MAIN(CollectionSchedulerTest) + +#include "collectionschedulertest.moc" diff --git a/autotests/server/fakecollectionscheduler.h b/autotests/server/fakecollectionscheduler.h new file mode 100644 --- /dev/null +++ b/autotests/server/fakecollectionscheduler.h @@ -0,0 +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 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/autotests/server/fakecollectionscheduler.cpp b/autotests/server/fakecollectionscheduler.cpp new file mode 100644 --- /dev/null +++ b/autotests/server/fakecollectionscheduler.cpp @@ -0,0 +1,55 @@ +/* + 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::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/src/server/collectionscheduler.h b/src/server/collectionscheduler.h --- a/src/server/collectionscheduler.h +++ b/src/server/collectionscheduler.h @@ -57,6 +57,13 @@ void setMinimumInterval(int intervalMinutes); Q_REQUIRED_RESULT int minimumInterval() const; + /** + * @return the timestamp (in seconds since epoch) when collectionExpired + * will next be called on the given collection, or 0 if we don't know about the collection. + * Only used by the unittest. + */ + uint nextScheduledTime(qint64 collectionId) const; + protected: void init() override; void quit() override; @@ -67,20 +74,29 @@ * @return Return cache timeout in minutes */ virtual int collectionScheduleInterval(const Collection &collection) = 0; + /** + * Called when it's time to do something on that collection. + * Notice: this method is called in the secondary thread + */ virtual void collectionExpired(const Collection &collection) = 0; void inhibit(bool inhibit = true); -protected Q_SLOTS: +private Q_SLOTS: void schedulerTimeout(); void startScheduler(); void scheduleCollection(/*sic!*/ Collection collection, bool shouldStartScheduler = true); -protected: - QMutex mScheduleLock; - QMultiMap mSchedule; +private: + using ScheduleMap = QMultiMap; + ScheduleMap::const_iterator constFind(qint64 collectionId) const; + ScheduleMap::iterator find(qint64 collectionId); + ScheduleMap::const_iterator constLowerBound(qint64 collectionId) const; + + mutable QMutex mScheduleLock; + ScheduleMap mSchedule; PauseableTimer *mScheduler = nullptr; - int mMinInterval; + int mMinInterval = 5; }; } // namespace Server diff --git a/src/server/collectionscheduler.cpp b/src/server/collectionscheduler.cpp --- a/src/server/collectionscheduler.cpp +++ b/src/server/collectionscheduler.cpp @@ -48,7 +48,7 @@ void start(int interval) { - mStarted = QDateTime::currentDateTime(); + mStarted = QDateTime::currentDateTimeUtc(); mPaused = QDateTime(); setInterval(interval); QTimer::start(interval); @@ -72,7 +72,7 @@ return; } - mPaused = QDateTime::currentDateTime(); + mPaused = QDateTime::currentDateTimeUtc(); QTimer::stop(); } @@ -86,7 +86,7 @@ start(qMax(0, remainder)); mPaused = QDateTime(); // Update mStarted so that pause() can be called repeatedly - mStarted = QDateTime::currentDateTime(); + mStarted = QDateTime::currentDateTimeUtc(); } bool isPaused() const @@ -106,15 +106,14 @@ CollectionScheduler::CollectionScheduler(const QString &threadName, QThread::Priority priority, QObject *parent) : AkThread(threadName, priority, parent) - , mScheduler(nullptr) - , mMinInterval(5) { } CollectionScheduler::~CollectionScheduler() { } +// Called in secondary thread void CollectionScheduler::quit() { delete mScheduler; @@ -139,8 +138,19 @@ return mMinInterval; } +uint CollectionScheduler::nextScheduledTime(qint64 collectionId) const +{ + QMutexLocker locker(&mScheduleLock); + const auto i = constFind(collectionId); + if (i != mSchedule.cend()) { + return i.key(); + } + return 0; +} + void CollectionScheduler::setMinimumInterval(int intervalMinutes) { + // No mutex -- you can only call this before starting the thread mMinInterval = intervalMinutes; } @@ -156,58 +166,52 @@ void CollectionScheduler::collectionChanged(qint64 collectionId) { QMutexLocker locker(&mScheduleLock); - for (const Collection &collection : qAsConst(mSchedule)) { - if (collection.id() == collectionId) { - Collection changed = Collection::retrieveById(collectionId); - DataStore::self()->activeCachePolicy(changed); - if (hasChanged(collection, changed)) { - if (shouldScheduleCollection(changed)) { - locker.unlock(); - // Scheduling the changed collection will automatically remove the old one - scheduleCollection(changed); - } else { - locker.unlock(); - // If the collection should no longer be scheduled then remove it - collectionRemoved(collectionId); - } + 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); } - - return; } + } else { + // We don't know the collection yet, but maybe now it can be scheduled + collectionAdded(collectionId); } - - // We don't know the collection yet, but maybe now it can be scheduled - collectionAdded(collectionId); } void CollectionScheduler::collectionRemoved(qint64 collectionId) { QMutexLocker locker(&mScheduleLock); - for (const Collection &collection : qAsConst(mSchedule)) { - if (collection.id() == collectionId) { - const uint key = mSchedule.key(collection); - const bool reschedule = (key == mSchedule.constBegin().key()); - mSchedule.remove(key); - locker.unlock(); - - // If we just remove currently scheduled collection, schedule the next one - if (reschedule) { - startScheduler(); - } - - return; + 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; } - QMutexLocker locker(&mScheduleLock); if (mSchedule.isEmpty()) { // Stop the timer. It will be started again once some collection is scheduled mScheduler->stop(); @@ -220,12 +224,13 @@ mScheduler->start(qMax(0, (int)(next - QDateTime::currentDateTimeUtc().toTime_t()) * 1000)); } +// Called in secondary thread void CollectionScheduler::scheduleCollection(Collection collection, bool shouldStartScheduler) { QMutexLocker locker(&mScheduleLock); - auto i = std::find(mSchedule.cbegin(), mSchedule.cend(), collection); - if (i != mSchedule.cend()) { - mSchedule.remove(i.key(), i.value()); + auto i = find(collection.id()); + if (i != mSchedule.end()) { + mSchedule.erase(i); } DataStore::self()->activeCachePolicy(collection); @@ -235,18 +240,19 @@ } const int expireMinutes = qMax(mMinInterval, collectionScheduleInterval(collection)); + // TODO: port to qint64 and toSecsSinceEpoch uint nextCheck = QDateTime::currentDateTimeUtc().toTime_t() + (expireMinutes * 60); // 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 - QMap::iterator it = mSchedule.lowerBound(nextCheck); - if (it != mSchedule.end() && it.key() - nextCheck < 60) { + auto it = constLowerBound(nextCheck); + if (it != mSchedule.cend() && it.key() - nextCheck < 60) { nextCheck = it.key(); // Also check whether there's another checked scheduled within a minute before // this one. - } else if (it != mSchedule.begin()) { + } else if (it != mSchedule.cbegin()) { --it; if (nextCheck - it.key() < 60) { nextCheck = it.key(); @@ -260,6 +266,23 @@ } } +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(qint64 collectionId) const +{ + return mSchedule.lowerBound(collectionId); +} + +// Called in secondary thread void CollectionScheduler::init() { AkThread::init(); @@ -293,16 +316,18 @@ startScheduler(); } +// Called in secondary thread void CollectionScheduler::schedulerTimeout() { + QMutexLocker locker(&mScheduleLock); + // Call stop() explicitly to reset the timer mScheduler->stop(); - mScheduleLock.lock(); const uint timestamp = mSchedule.constBegin().key(); const QList collections = mSchedule.values(timestamp); mSchedule.remove(timestamp); - mScheduleLock.unlock(); + locker.unlock(); for (const Collection &collection : collections) { collectionExpired(collection);